Java垃圾回收机制
1.Java垃圾回收整理
2.CMS收集器
一种以获取最短回收停顿时间为目标的收集器。大部分Java应用希望系统停顿时间最短,使用CMS收集器。
2.1 CMS垃圾回收过程
CMS基于标记-清除
算法,整个过程分为四步:
初始标记,并发标记,重新标记,并发清除
初始标记和重新标记两个步骤仍然需要Stop The World。
初始标记仅仅标记一下GC Roots能直接关联的对象,速度很快;
并发标记就是进行GC Roots Tracing的过程;
重新标记则是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段停顿比初始标记稍微长,但远比并发标记的时间短;
整个过程耗时最长的并发标记和并发清除过程,收集器都可以与用户线程一起工作。总统来说,CMS收集器的内存回收过程与用户线程是一起并发执行的。
2.2 CMS特点
CMS特点:并发收集,低停顿。
缺点:
1.CMS收集器对CPU资源非常敏感。默认启动的回收线程数是(CPU+3)/4。当CPU4个以上时,并发回收垃圾收集线程不少于25%的CPU资源。
2.CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure
失败而导致另一次Full GC的产生。由于CMS并发清理时,用户线程还在运行,伴随产生新垃圾,而这一部分出现在标记之后,只能下次GC时再清理,这部分垃圾称为“浮动垃圾”。
由于CMS运行时还需要给用户空间继续运行,则不能等待老年代几乎被填满再进行收集,需要预留一部分空间提供并发收集时用户程序运行。JDK1.6中,CMS启动阈值为92%。若预留内存不够用户使用,则出现一次Concurent Mode Failure
失败,这时虚拟机启动后备预案,临时启动Serial Old收集老年代,这样停顿时间很长。
3.CMS基于标记-清除
算法实现,会产生大量空间碎片,空间碎片过多时,没有连续空间分配给大对象,不得不提前触发一次Full GC
。当然可以开启-XX:+UseCMSCompactAtFullCollection
(默认开启),在CMS顶不住要Full GC时开启内存碎片合并整理过程。内存整理过程是无法并发的,空间碎片问题没了,但停顿时间变长。
2.3 CMS一共会有几次STW?
1.初始标记和重新标记各需要一次;
2.CMS并发的代价是预留空间给用户线程,预留不足的时候出发Full GC,这时Serail Old会STW;
3.CMS是标记-清除算法,导致空间碎片,如果没有连续空间分配给大对象时,会触发Full GC,Full GC会开始碎片整理,也会STW。
2.4 CMS什么时候Full GC?
1.老年代空间不足
老年代空间只有在新生代对象转入及创建为大对象,大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,会抛出异常:java.lang.OutOfMemoryError:Java heap space
,为避免以上两种状况引起的Full GC,调优时尽量做到让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2.Permanet Generatiion空间满
PermanetGeneration中存放的是一些class的信息,当系统中要加载的类,反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下不会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出异常:java.lang.OutOfMemoryError:PermGen space
,为了避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3.CMS GC时出现promotion failed
和concurrent mode failure
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下,对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
应对措施为:增大survivor sapce,老年代空间或调低触发并发GC的比率。
4.统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象,在进行Minor GC时,做一个判断,如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到老年代,那么当下一次Minor GC发生时,首先检查老年代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
3.G1收集器
3.1G1关键概念
3.1.1 Region
G1将堆划分为一个个相等的不连续的内存区域,每个region都有一个分代的角色:eden,suviver,old(old中还有一种细分的humongous,用来存放大小超过region50%的巨型对象)。
但是对每个角色的数量并没有强制的限定,也就是每种分代内存的大小,可以动态变化(默认新生代占整个堆的5%)。
3.1.2 SATB
SATB的全称是Snapchat-At-The-Beginning。SATB是维持并发GC的一种手段,G1并发的基础就是SATB。可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图。在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
如何找到在GC的过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,因而被视为隐式marked。通过这种方式就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。
解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?G1是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了环切。通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化。
3.1.3 RSet
RSet全称是Remember Set,每个region都有一个RSet,记录的是其他region中的对象引用本region对象的关系。G1里面还有另外一种数据结构Collection Set(CSet),CSet记录的是GC要收集的region的集合,CSet里的region可以是任意代的。在GC的时候,对于old->young和old->old的跨对象引用,只要扫描对应的CSet中的RSet即可。
3.1.4 停顿预测模型
G1通过一个停顿预测模型来根据用户配置的停顿时间来选择CSet的大小,从而达到用户期待的应用程序暂停时间。通过-XX:MaxFCPauseMillis
参数来设置。这一点类似于ParallelScavenge收集器。关于停顿时间的设置并不是越短越好,设置的时间越短意味着每次收集的CSet越小,导致垃圾逐步积累变多,最终不得不退化成Serial GC;停顿时间设置的过长,会导致每次都产生长时间的停顿,影响了程序对外的响应时间。
3.1.5 三色标记算法
并发标记的三色标记算法,它是描述追踪式回收器的一种游泳的方法,利用它可以推演回收器的正确性。
首先将对象分为三种类型的:
黑色:根对象,或者该对象与它的子对象都被扫描;
灰色:对象本身被扫描,但还没扫描完该对象中的子对象;
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。
3.2 G1特点
G1最大的特点是高效的执行回收,优先去执行那些大量对象可回收的区域。
G1使用了gc停顿可预测模型,来满足用户设定的gc停顿时间,根据用户设定的目标时间,G1会自动的选择哪些region要清除,一次清除多少个region。
G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理,清除内存(copiong算法)。
对比之前的垃圾收集器:
对比使用mark-sweep的CMS,G1使用的coping算法不会造成内存碎片;
对比Parallel Scavenge(基于coping),Parallel Old(基于mark-compact),会对整个区域做整理导致GC Pause会比较长,而G1只是特定的整理几个region。
G1的设计原则是简单可行的性能调优,声明以下参数即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC
为开启G1垃圾收集器,-Xmx32g
设置堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200
设置GC的最大暂停时间为200ms。如果需要调优,只需要修改最大暂停时间即可。
3.3 G1垃圾回收过程
分两个步骤:
1.全局并发标记(global concurrent marking);
2.拷贝存活对象(evacuation)。
3.3.1 全局并发标记
全局并发标记阶段是基于SATB的,主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。它的执行过程分为五个步骤:
1.初始标记(initial mark,STW):扫描根集合,将所有通过根集合直达的对象压入扫描栈,等待后续处理;
2.并发标记(Concurrent Marking):G1 GC在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW新生代垃圾回收中断;
3.最终标记(Remark,STW):该阶段处理在并发标记阶段write barrier记录下的引用,同时进行弱引用的处理;
4.清除垃圾(Cleanup,STW):清点和重置标记状态。这个阶段不会实际去做垃圾收集,只是根据停顿模型预测出CSet,等待evacuation阶段来回收。
3.3.2 拷贝存活对象阶段
该阶段是全暂停的,把一部分region里的活对象拷贝到另一部分region中,从而实现垃圾的回收清理。从第一阶段选出来的region中筛选出任意多个region作为垃圾回收的目标,这些要收集的region叫CSet,通过RSet实现。
筛选出CSet之后,G1并行的将这些region里的存活对象拷贝到其他region中,这点类型于ParallelScavenge的拷贝过程,整个过程完全暂停。关于停顿时间的控制,就是通过选择CSet的数量来达到控制时间长短的目标。
3.4 G1收集模式
G1提供两种GC模式,Young GC和Mix GC,两种都是Stop The World(STW)的。
两种模式都只是并发拷贝的阶段。
G1的运行过程是这样的,会在Young GC和Mix GC之间不断的切换运行,同时定期的做全局并发标记,在实在赶不上回收速度的情况下使用Full GC(Serial GC)。初始标记是搭在Young GC上执行的,在进行全局并发标记的时候不会做Mix GC,在做Mix GC的时候也不会启动初始标记阶段。当Mix GC赶不上对象产生的速度的时候就退化成Full GC,这里是需要重点调优的地方。
3.4.1 Young GC
收集新生代里的region。
Young GC阶段:
1.根扫描:静态和本地对象被扫描;
2.更新RS:处理dirty card队列,更新RS;
3.处理RS:检测从新生代到老年代的对象;
4.对象拷贝:拷贝存活的对象到survivor/old区域;
5.处理引用队列:软引用,弱引用,虚引用等。
3.4.2 Mix GC
收集新生代的所有region+全局并发标记阶段选出的region。
3.5 G1最佳实践
3.5.1 调优暂停时间指标
通过-XX:MaxGCPauseMillis=x
可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间。一般情况下这个值设置为100ms或者200ms都是可以的,但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致G1跟不上垃圾产生的速度,最终退化成Full GC。
3.5.2 不要设置新生代和老年代的大小
G1收集器在运行的时候会调整新生代和老年代的大小。通过改变代的大小来调整对象晋升的速度以及晋升年龄,从而达到我们为收集器设置的暂停时间目标。设置了新生代多大小相当于放弃了G1为我们做的自动调优。我们只需要设置堆内存的大小,剩下的交给G1去分配各个代的大小。
3.5.3 关注Evacuation Failure
Evacuation Failure类型于CMS里面的晋升失败,堆空间的垃圾太多导致无法完成region之间的拷贝,于是不得不退化成Full GC来做一次全局范围的垃圾回收。
4. GC调优
-
查看默认的
java -XX:+PrintCommandLineFlags -version
JDK1.8 打印结果
-XX:InitialHeapSize=260281024 -XX:MaxHeapSize=4164496384 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
JDK1.8默认垃圾收集器是新生代并行收集+老年代串行收集。
-
调优
//基础配置 -Xmx512m //堆最大可用内存 -Xms128m //堆初始内存,当与-Xmx相同,避免每次垃圾回收完成后JVM重新分配内存 -Xmn128m //年轻代大小,整个JVM内存大小=年轻代大小 + 老年代大小 + 永久代大小, -Xss256k //每个线程的堆栈大小,默认为1m -XX:PermSize=50m //永久代大小,JDK8中无效 -XX:MaxPermSize=128m //永久代最大值,JDK8中无效 -XX:+DisableExplicitGC //禁止代码中显式调用GC,相当于禁用System.GC(),调用该函数会引发一次stop-the-world的full GC,对整个GC堆做收集 //CMS相关配置 -XX:+UseConcMarkSweepGC //设置CMS并发收集器,年轻代自动设置为ParNewGC -XX:+CMSParallelRemarkEnabled //开启并行remark,减少重新标记阶段的暂停时间 -XX:+UseCMSCompactAtFullCollection //打开对老年代的压缩 -XX:LargePageSizeInBytes=64m //单个内存页的大小 -XX:+UseFastAccessorMethods //原始类型的快速优化 //两个一起使用,降低CMS GC的频率 -XX:+UseCMSInitiatingOccupancyOnly //只使用设置的回收阈值,也就是下面的70,如果不指定,JVM仅在第一次使用设定值,后续则自动调整 -XX:CMSInitiatingOccupancyFraction=70 //当老年代占满70%的时候进行CMS收集,默认CMS是在Tenured Generation(终身代/老年代)占满68%的时候进行CMS收集, -XX:+HeapDumpOnOutOfMemoryError //JVM在出现内存溢出的时候Dump出当前的内存转储快照