体系结构 内存模型
JVM内存区域模型
1.方法区
方法区包括存储虚拟机加载的类信息的区域和运行时常量池
虚拟机加载的类信息
类型信息:类的完整名称(eg:java.lang.String);类的直接父类的完整名称;类的直接实现接口的有序列表(因为一个类直接实现的接口可能不止一个,因此放到有序列表中);类的修饰符
可以看做对类的一个信息登记:比如这个类的名字叫啥,父类是谁,有没有实现接口,权限是啥
类型的常量池(即运行时常量池):每个Class文件中都维护这一个常量池(保存在类文件里,非方法区的运行时常量池),里面存放着编译时间生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池;
字面值:(就像是String-基本数据类型)以及它们的包装类的值,以及final修饰的变量,简单来说就是在编译期间就可以确定下来的值
符号引用:不同于我们常说的引用,它们是对于类型、域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用
存在这里面的数据,类似于保存在数组中,外部根据索引来获得它们
字段信息:声明的顺序;修饰符;类型;名字
方法信息:生命的顺序;修饰符;返回值类型;名字;参数列表(有序保存);异常表(方法抛出的异常);方法字节码(native、abstract方法除外);操作数栈和局部变量表的大小
类变量(即static变量):非final类变量(在Java虚拟机使用一个类之前,它必须在方法区中为每个非final类变量分配空间;非final类变量存储在定义它的类中)
final类变量不储存在这里,由于final的不可更改性,因此,final类变量的值在编译期间就被确定了,因为被保存在类的常量池里面,然后在加载类的时候复制进方法区的运行时常量池里面;final类变量储存在运行时常量池里面,每一个使用它的类保存着对它的引用
指向类加载器的引用:JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中;
JVM在动态链接的时候需要这个信息。当解析一个类型到另一个类型引用的时候,JVM需要保证这两个类型的类加载器是相同的。这对JVM区分名字空间的方式是至关重要的
指向Class类的引用:JVM为每个加载的类都创建一个java.lang.Class的实例(储存在堆上)。而JVM必须以某种方式把Class的这个实例和储存在方法区的类型数据(类的元数据)联系起来,因此,类的元数据里面保存了一个Class对象的引用
方法表:是一组对类实例方法的直接引用(包括从父类继承的方法,JVM可以通过方法表快速激活实例方法)
运行时常量池:
方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中
对象访问
Object ocjectRef = new Object()
“Object objectRef”这部分将会反映到Java栈的本地变量,作为一个reference类型数据存在
“new Object()”这部分将会反映到Java堆中,存储Object类型所有实例数据值的结构化内存,长度不固定;还保存有能查到此对象类型数据(如对象类型、父类、实现的方法、接口等)的地址信息;这些数据储存在方法区中
reference类型只规定了一个指向对象的引用地址,访问方式主要通过两种:
句柄访问方式:Java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据的各自的具体地址信息
指针访问方式:reference变量中直接存储的就是对象的地址,而Java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据
句柄访问方式的优势是reference中存储的是稳定的句柄地址,在对象移动时只需要更改句柄中的实例数据指针,而reference不需要改变
指针访问方式的优势是访问速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是指针访问方式
垃圾收集算法
引用计数收集器(未用)
引用计数收集器采用的是分散式管理方式,通过计数器记录对象是否被引用。当计数器为0时说明此对象不再被使用,可以被回收
主要缺点:循环使用的场景无法实现回收,例如Object A分别引用Object B和Object C,B和C相互引用,那么即使A释放了对B和C的引用,也无法回收。SUNJDK在实现GC的时候未采用这种方式
跟踪收集器
跟踪收集器采用的是集中式管理方式,全局记录对象之间的引用状态,执行时从一些列GC Roots的对象作为起点,从这些节点向下开始进行搜索所有的引用链,当一个对象到GC Roots没有任何引用链时,证明该对象是不可用的。
eg:下图中,对象Object 6、Object 7和Object 8虽然相互引用,但是他们的GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
可以作为GC Roots的对象:
虚拟机栈(栈帧中的本地变量)中的引用对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用对象
实现算法:标记-清除;复制;标记-压缩
标记-清除
标记-清除算法是最基础的算法,其他收集算法都是基于此思想。标记-清除算法氛围“标记”和“清除”两个阶段
首先标记出需要回收的对象,标记完成之后同意清除对象
主要缺点:1.标记和清除的效率不高;2.标记清除之后会产生大量不连续的内存碎片
复制算法
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效
主要缺点:内存缩小为原来的一半
标记-整理算法
标记-整理算法和标记-清除算法一样,后续操作不是直接清理对象,而是在清理无用对象之后让所有存活的对象都向一端移动,并更新引用其对象的指针
主要缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片
HotSpot JVM收集器
HotSport中有7中收集器,分为新生代收集器和老年代收集器。如果两个收集器之前存在连线,就说明它们可以搭配使用。
从Java3开始,HotSpot团队一直努力朝着高效收集、减少停顿(STW:Stop The World)的方向努力,也贡献了从串行到CMS乃至最新的G1在内的一系列的优秀的垃圾收集器。下面就几种典型的组合进行简单的介绍
串行收集器
串行收集器组合:Serial + Serial Old
开启选项: -XX: +SerialGC
串行收集器是最基本、发展时间最长、久经考验的垃圾收集器,也是client模式下的默认收集器配置
串行收集器采用单线程Stop-the-world的方式进行收集。当内存不足时,串行GC设置停顿标识,待所有线程都进入安全点时,应用线程暂停,串行GC开始工作,采用单线程方式回收空间并释放内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。事实上,串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。
并行收集器
并行收集器组合 Parallel Scavenge + Parallel Old
开启选项: -XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可相互激活)
并行收集器是以关注吞吐量为目标的垃圾收集器,也是Server模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代Parallel Scavenge收集器上
并行收集器与串行收集器工作模式相似,都是stop-the-world方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。关注吞吐量主要指年轻代的Parallel Scavenge收集器,通过两个目标参数 -XX:MaxGCPauseMills 和 -XX:GCTimeRatio,调整新生代空间大小,来降低GC触发的频率。并行收集器适合对吞吐量要求远远高于延迟要求的场景,并且在满足最差延迟的情况下,并行收集器将提供最佳的吞吐量。
并行标记清除收集器
并发标记清除收集器组合:ParNew + CMS + Serial Old
开启选项:-XX:+UseConcMarkSweepGC
并发标记清除(CMS)是以关注延迟为目标,十分优秀的垃圾回收算法,开启后年轻代使用STW式的并行收集,老年代回收采用CMS进行垃圾回收,对延迟的关注也体现在老年代CMS上。
年轻代ParNew与并行收集器类似,而老年代CMS每个收集周期都要经历:初始标记、并发标记、重新标记、并发清除。其中,初始标记以STW的方式标记所有的对象;并发标记则同应用线程一起并行,标记出跟对象的可达对象;在进行垃圾回收前,CMS再以一个STW进行重新标记,标记那些由mutator线程(指引起数据变化的线程,即应用线程)修改而可能错过的可达对象;最后得到的不可达对象将在并发清除阶段进行回收。值得注意的是,初始标记和重新标记都已有华为多线程执行。CMS非常适合堆内存大、CPU核数多的服务端应用,也是G1出现之前大型应用的首选收集器。
但是CMS并不完美,它有一下缺点:
1.由于并发进行,CMS在收集与应用线程同时会增加对堆内存的占用,也就是说,CMS必须在老年堆内存用尽之前进行垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。
2.标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制进对堆内存进行压缩。CMS也提供了参数 -XX: CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
G1(Garbage First)
开启选项: -XX: +UseG1GC
之前介绍的垃圾回收器组合,都有几个特点:
1.年轻代、老年代是独立且连续的内存块;
2.年轻代收集使用单eden、双survivor进行复制算法;
3.老年代收集必须扫描整个老年代区域;
4.都是以尽可能少而快地执行GC为设计原则;
G1收集器也是已关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制。但是G1收集器和前三种都有很大的不同:
1.G1的设计原则是“首先收集尽可能多的垃圾(Garbage First)”。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是内部采用了启发式算法,在老年代找出具有高收集收益的分区冒进行收集。同时,G1可以根据用户设置的暂停时间目标自动调整年轻代和总队大小,暂停目标越短年轻代空间越小、总空间越大;
2.G1采用内存分区(region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然是一种压缩方案(局部压缩);
3.G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随着G1的运行在不同代之后前后切换;
4.G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集即可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
JVM内存调优
JVM调优主要针对内存管理方面的调优,包括控制各个代的大小,GC策略。由于GC开始垃圾回收时会挂起应用线程,严重影响了性能,调优的目的就是为了尽量降低GC所导致的应用线程暂停时间、减少FULL GC的次数。
代大小调优
-Xms和-Xmx通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM Heap所能使用的最大的内存。
-Xmn决定了新生代空间的大小,新生代Ede、S0、S1三个区域的比例可以通过-XX:SuvivorRatio来控制(加入值为4,表示Eden:S0:S1 = 4:3:3)
-XX:MaxTenuringThreschold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC中有效
-XX:PermSize、-XX:MaxPermSize 来控制方法区的大小,通常设置为相同的值
1.避免新生代设置过小
当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致 minor GC对象提前进入老年代。当老年代内存不足时,会触发Full GC
2.避免新生代设置过大
新生代设置过大时会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是minor GC执行回收的时间大幅度增加
3.避免Suvivor区过大或过小
-XX:SurvivorRatio 参数的值越大,意味着Eden的区域变大,minor GC的次数会降低,但两块Suvivor区域变小,如果超过Survivor区域内存大小的对象在minor GC后仍未被回收,则会直接进入老年代
-XX:SurvivorRatio 参数的值越小,意味着Eden的区域变小,minor GC的次数会增加,Survivor区域会变大,意味着可以存储更多在minor GC下存活的对象,避免其进入老年代
4.合理设置对象在新生代存活的周期
新生代存活周期的值决定了新生代对象在经历过多少次minor GC后进入老年代。因此这个值要根据自己的应用来调优,JVM参数上这个值对应的-XX:MaxTenuringThreshold,默认为15次
文章内容来自学习论坛内容的总结:https://bbs.csdn.net/topics/390251794