jvm探究(Java Virtual Machine)------java虚拟机模型
一、JVM的基本介绍
JVM 是 Java Virtual Machine 的缩写,它是一个java实现的虚拟计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现···
好,其实抛开这么专业的句子不说,就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。
1.Java文件是如何被运行的
比如我们现在写了一个 HelloWorld.java ,而我们的 JVM 是不认识文本文件的,所以它需要一个 编译 ,让其成为一个它会读二进制文件的 HelloWorld.class
① 类加载器
如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个 类加载器 中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进JVM里面来。
② 方法区(Method Area)
方法区 是用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,编译后代码···等
类加载器将 .class 文件搬过来就是先丢到这一块上
③ 堆(Heap)
堆 主要放了一些存储的数据,比如对象实例,数组···等,它和方法区都同属于 线程共享区域 。也就是说它们都是 线程不安全 的
jvm的调优就是在调线程共享区域。大部分调堆
④ 栈(Java栈Stack)
栈 这是我们的代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。
我们会听说过 本地方法栈(Native Method Stack) 或者 本地方法接口 这两个名词,不过我们基本不会涉及这两块的内容,它俩底层是使用C来进行工作的,和Java没有太大的关系。
⑤ 程序计数器
主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是 线程独享 的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。
线程独享的这个区域是不会进行垃圾回收的
小总结
- Java文件经过编译后变成 .class 字节码文件
- 字节码文件通过类加载器被搬运到 JVM 虚拟机中
- 虚拟机主要的5大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行
2.一个java文件运行举例
一个简单的学生类
执行main方法的步骤如下:
- 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
- JVM 找到 App 的主程序入口,执行main方法
- 这个main中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个Student对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
- 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 指向方法区中的 Student 类的类型信息 的引用
- 执行student.sayName();时,JVM 根据 student 的引用(变量名在栈中)找到 student 对象(堆中),然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
- 执行sayName()
其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。
二、类加载器的介绍
1.类加载器的作用
- 作用:负责加载.class文件到内存中
它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容中静态数据结构转换成运行时数据区的方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine(执行引擎) 来决定
2.类加载器的流程
从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接
3.加载
-
在堆中生成一个代表这个类的 java.lang.class对象,作为方法区这些数据的访问入口
- 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的
loadClass()
方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
- 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的
4.连接
- 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
- 准备
- 解析
- 虚拟机将常量池内的符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。(举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。)
- 虚拟机将常量池内的符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
5.初始化
-
初始化其实就是执行类构造器的
<clinit>()
方法的过程,而且要保证执行前父类的<clinit>()
方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。(例子:此时准备阶段时的那个public static int value=111
由默认初始化的0变成了显式初始化的111. 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。) -
对类进行初始化(只有主动去使用类才会初始化类):
-
当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
-
使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forname("..."), newInstance()等等。如果类没初始化,需要触发其初始化。 -
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
= = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =
-
MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
-
「补充,来自issue745」 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
= = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =
-
6.卸载
-
GC将无用对象从内存中卸载
-
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:
- 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被GC
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,jdk自带的BootstrapClassLoader, ExtClassLoader, AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
7.类加载器的双亲委派模型
-
类加载器对字节流class文件的加载需要靠classLoader
- JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自
java.lang.classLoader
:
- JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自
-
双亲委派模型
- 双亲委派机制的好处
- 如果我们不想用双亲委派模型怎么办?
三.jvm(java虚拟机)的内存分析
1.运行时数据区(java内存区域划分)----物理层面
-
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
见图如下:
-
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
2.程序计数器(PC寄存器:Program Counter Register)
-
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
-
程序计数器的作用
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
-
注意:程序计数器是唯一一个不会出现
OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
3.本地方法栈(Native Method Stack)
关键在于native关键字
(现在可以通过调用其他接口进行融合:比如Socket http~)
4.虚拟机栈(Stack)
-
虚拟机栈和本地方法栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
-
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
-
栈和队列
-
栈中可以存放:8大基本类型的变量+对象的引用变量(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置))+实例的方法(包括main方法---->>第一个入栈的)
-
Java 虚拟机栈会出现两种错误:
StackOverFlowError
和OutOfMemoryError
。StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
(通常是程序中存在死循环或者深度递归调用造成的)
OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常异常。
(通常是栈大小设置太小)
-
解决方法:
- 可以通过虚拟机参数 -Xss 来设置每个线程栈的大小。
5.方法区(非堆)
-
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造函数,接口定义)、常量(final)、静态变量(static)、运行时的常量池
-
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
-
理解运行时常量池和字符串常量池:
-
运行时常量池是在类加载器的加载步骤完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类加载器在连接步骤的解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
-
字符串池里的内容是在类加载器的加载步骤完成之后,经过类加载器的连接步骤中的验证,准备阶段之后,如果有创建一个对象实例中包含了字符串,那么就会在堆中生成该对象实例和生成字符串对象实例(字符串也是个对象),然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)
public class test(){ private int a; private Sting name="lyj"; } public static void main(String[] args){ Test test1=new test(); } //此处创建的实例是new test(),因为该对象中包含了字符串,所以又要在堆中创建一个字符串对象实例,而字符串对象实例的引用值“lyj”会存放到字符串常量池中
-
-
理解方法区和永久代的关系:
-
hotspot移除了永久代 (PermGen) 用元空间(Metaspace)取而代之的原因:
- 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,可以进行调整,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
当你元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
标志定义元空间的初始大小,如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。- 元空间(元空间不在虚拟机中,在本地内存)里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
-
-
注意:对象的实例变量存放在堆内存中,字符串常量池在JDK1.7后也放入了堆内存中,和方法区无关
6.堆
- Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。一个JVM只有一个堆内存,堆内存大小是可以调节的,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
6.1、堆中的垃圾回收流程:
-
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间(伊甸园)、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
-
-
继续存放对象,也是首先在Eden区(伊甸园)存放,当Eden满之后,又触发Minor GC,原先form指针指向的survivor0区存活的对象会被复制到to指针指向的survivor1区,Eden区存活的对象也会放入to指针指向的survivor1区,然后form to指针进行互换
这时form指针指向的survivor1区的 里面有对象,这时to指针指向的survivor0区的 里面没有有对象
-
此处包含 动态对象年龄判定 的知识点
-
当survive区满,触发Minor GC(进行垃圾回收),存活下来的对象它的年龄会增加,当增加到一定程度(默认为15岁,因为年龄标志位只有4位,最多为15),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。或者
-
Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”,并且该年龄段开始及大于的年龄对象就要进入老年代。
或者
-
survive区中Minor GC 一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。
经过多次的 Minor GC后仍然存活的对象,就会移动到老年代
- 注意1:大对象直接进入老年代,因为大对象需要大量连续内存空间的对象(比如:字符串、数组)。这样做可以避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
- 注意2:要存入一个名为allocation1的对象,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。
老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。
-
当老年区执行了full gc之后仍然无法进行对象保存的操作,就出现了堆中最容易出现的 OutOfMemoryError 错误,这就是虚拟机中的堆内存不足,并且出现这种错误之后的表现形式还会有几种,比如:
6.2、解决虚拟机--堆中出现的OOM错误
- 可能会是堆内存设置的大小过小,这个可以通过虚拟机参数-xms(初始堆大小)、-Xmx(最大堆大小)来尝试扩大堆内存进行调整
- 使用内存监控软件(内存快照分析工具MAT\jprofiler)查找程序中的错误代码(未看)
7.直接内存 (非运行时数据区的一部分)
-
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
- 因为:本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
-
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
四.GC:垃圾回收
1.垃圾回收的区域
- 程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。
2.主要的GC(垃圾回收)的分类
-
部分收集 (Partial GC):
-
整堆收集 (Full GC):收集整个 Java 堆和方法区。
3.垃圾回收前判断哪些对象还存活
-
2.1)引用计数法(引用计数器)
-
2.2)可达性分析算法
-
这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中。搜索当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。
-
优点:可以解决循环引用的问题
-
缺点:它的实现需要耗费大量资源和时间,同时它的分析过程引用关系不能发生变化,所以需要停止所有进程
-
4.垃圾回收算法
4.1标记-清除算法
4.2标记-压缩(整理)算法
4.3标记-复制算法
-
为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。
- 缺点:
- 浪费了(一半)内存空间,堆内存的使用效率就会变得十分低下
- 在极端情况下,对象的存活率高,复制存活的对象到另外一个区域,也会造成效率低
- 缺点:
4.4每种算法的比较
4.5分代回收算法
- 把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
4.6了解一些垃圾回收器
HotSpot VM中的垃圾回收器,以及适用场景 [
-
Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
-
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
-
特点:并发收集、低停顿
-
缺点:CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片
-
-
从jdk9开始,G1(整堆回收器)成为默认的垃圾收集器 目前来看,G1回收器停顿时间最短而且没有明显缺点,非常适合Web应用。
- 特点:
- 并行与并发:G1 能充分利用 cpu、多核环境下的硬件优势,使用多个 cpu(cpu 或者 cpu 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
- 特点:
-
= == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =
并行:真正的“同时”运行,在同一时刻,有多个任务同时执行。(例如,在多核处理器上,有两个线程同时执行同一段代码。
并发:两个或多个任务可以在重叠的时间段内启动,运行和完成
并行(多个线程同时执行) 一定是并发,并不一定意味着并发一定要求是并行(包含关系)
-
= == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =
五.JVM调优
对JVM进行调优,主要针对堆内存
1.jvm调优工具
2.jvm调优参数
2.1 调整最大堆内存和最小堆内存
-Xmx –xms:指定java堆最大值(默认值是物理内存的1/4(<1GB))和初始java堆最小值(默认值是物理内存的1/64(<1GB))
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -xms的最小限制。简单点来说,你不停地往堆内存里面丢数据,等它剩余大小小于40%了,JVM就会动态申请内存空间不过会小于-Xmx,如果剩余大小大于70%,又会动态缩小不过不会小于–xms。就这么简单
2.2调整新生代和老年代的比值
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
2.3 调整Survivor区和Eden区的比值
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
// -Xmn5120m:代表新生代
//-XXSurvivorRatio=3:代表Eden:Survivor = 3
根据Generation-Collection算法(目前大部分JVM采用的算法),一般根据对象的生存周期将堆内存分为若干不同的区域,一般情况将新生代分为Eden ,两块Survivor;
//计算Survivor大小, Eden:Survivor = 3,总大小为5120,3x+x+x=5120 x=1024
根据实际事情调整新生代、幸存代的大小,官方推荐新生代占java堆的3/8,幸存代占新生代的1/10
六.理解String类和常量池
1.String 对象的两种创建方式:
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
这两种不同的创建方法是有差别的。
- 第一种方式是在常量池中拿对象;
- 第二种方式是直接在堆内存空间创建一个新的对象。
记住一点:只要使用 new 方法,便需要创建新的对象
2.String s1 = new String("abc");这句话创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建一个“abc”对象,然后在堆空间中再创建一个String类的对象--字符串常量“abc”,因此将创建总共 2 个字符串对象。
验证:
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
3.字符串的拼接
- 字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的。
- StringBuffer:使用append实现
StringBuilder:使用append实现。
- StringBuffer:使用append实现
- String字符串拼接通过StringBuilder走中间过程,通过append方法实现。
4.String和StringBuffer和StringBuilder的区别
- String是值不可变类,每次在String对象上的操作都会生成一个新的对象;
- StringBuffer和StringBuilder则允许在原来对象上进行操作,而不用每次增加对象;StringBuffer是线程安全的,但效率较低,而StringBuilder效率最高,但非线程安全。
5.使用equals和'=='进行字符串比较的差异?
-
对于基本类型来说,== 比较的是值是否相等;
-
对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
-
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String,本身的equals方法就被重写了),则比较的是地址里的内容。
6.String类中intern()方法
作用:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象,一个是常量池中的 String 对象
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
JMM(Java Memory Model)----java内存模型
一、定义java内存模型的原因
二、JMM(java内存模型)和JVM的内存模型的区别
- JVM静态存储模型就是jvm内存模型:它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。
三、cpu缓存模型(计算机内存的缓存)
- 处理器(cpu): 寄存器:每个cpu都包含一系列寄存器,他们是cpu的基础,寄存器执行的速度,远大于在主存上执行的速度
- cpu高速缓存:由于处理器与内存访问速度差距非常大,所以添加了读写速度尽可能接近处理器的高速缓存,来作为内存与处理器之间的缓冲,将数据读到缓存中,让运算快速进行,当运算结束,再从缓存同步到主存中,就无须等待缓慢的内存读写了。cpu Cache 缓存的是内存数据用于解决 cpu 处理速度和内存不匹配的问题
- 内存:一个计算机包含一个主存,所有cpu都可以访问主存,主存通常远大于cpu中的缓存
- 运作原理: 通常,当一个cpu需要读取主存时,他会将主存的内容读取到缓存中,将缓存中的内容读取到内部寄存器中,在寄存器中执行操作,当cpu需要将结果回写到主存中时,他会将内部寄存器的值刷新到缓存中,然后会在某个时间点将值刷新回主存
原文链接:https://blog.csdn.net/wangnanwlw/article/details/86466782
四、java内存模型
-
java内存模型(Java Memory Model, JMM)是一种规范。它规范了java虚拟机与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值。以及在必须时如何同步的访问共享变量 。
(线程之间通信必须要通过主内存(主内存其实是jvm内存模型的堆内存))
-
java内存模型一般指的就是下图。
- 线程之间的共享变量存储在主内存里面,每个线程都有一个私有的本地内存(又称工作内存)
- 本地内存是java内存模型的抽象概念,它并不是真实存在的。本地内存存储了该线程以读或写共享变量拷贝的副本。(比如线程A 要使用主内存中的变量a,线程A会先拷贝出变量a 的副本存储在自己的本地内存中),它涵盖了缓存,写缓存区,寄存器以及其他硬件和编译器的优化。
- 线程对变量的操作都在自己的本地(工作)内存中,操作完成后再将变量更新至主内存;其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递