本章内容
CMS垃圾收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器主要用于要求低延迟(即:提高响应速度)的互联网项目。
设置CMS收集器参数:-XX:+UseConcMarkSweepGC。
CMS收集器采用的算法是标记-清除算法。
CMS垃圾收集器特点:
- 1)CMS只会回收老年代和永久代(JDK1.8为元数据区,CMS收集器默认不会对永久代进行垃圾回收,如希望对永久代进行垃圾回收,可通过设置参数:-XX:+CMSClassUnloadingEnabled开启对永久代的垃圾回收,该参数默认关闭),不会收集年轻代。
- 2)CMS是一种预处理垃圾回收器,它需要在老年代内存耗尽前完成垃圾回收,否则会导致并发回收失败(并发失败会退化为SerialOld单线程垃圾回收器),因此,CMS有一个触发垃圾回收的阀值(参数:-XX:CMSInitiatingOccupancyFraction,默认值为92%),即:老年代或永久代内存达到92%开始进行垃圾回收。
CMS垃圾回收过程
CMS垃圾回收过程主要分为初始标记、并发标记、并发预处理、可终止预处理、重新标记、并发清除、并发重置七个步骤。
初始标记
初始标记主要是标记存活的对象,存活对象包含两部分:
- 1)标记老年代中所有的GC Roots对象。
- 2)标记年轻代中引用老年代对象的存活对象。
如图所示:
为了加快初始标记阶段的处理速度,减少停顿时间,可以开启初始标记并行化,参数为:-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数(参数:-XX:ParallelGCThreads),线程数不要超过CPU核数。
注意:初始标记阶段会引发STW。
并发标记
并发阶段主要是沿着初始标记阶段标记的对象寻找存活对象,该阶段与应用程序并发运行。由于程序运行期间会发生新生代对象晋升到老年代、老年代直接分配对象以及老年代对象引用关系发生变更,因此,需要对这些对象进行重新标记(即:将这些对象所在的Card标识为Dirty),避免对象漏标,同时也避免扫描整个老年代。
如图所示:
注意:
- 1)并发标记阶段不会引发STW。
- 2)并发标记阶段与应用程序并发运行容易导致Concurrent Mode Failure。
三色标记
在并发标记阶段,由于标记期间与应用程序并行,对象间的引用关系可能发生变化,因此采用三色标记的方式对对象进行标记,标记过程分为三种颜色:白色、灰色、黑色。
- 黑色:表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过, 但该对象上至少存在一个引用还没有被扫描过。
- 白色:表示对象尚未被垃圾收集器访问过。 在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
标记过程:
- 1)初始时,所有对象都在 【白色集合】中。
- 2)将GC Roots直接引用到的对象转移【灰色集合】中。
- 3)从灰色集合中获取对象:
- 将本对象引用到的其他对象全部转移【灰色集合】中。
- 将本对象转移【黑色集合】中。
重复步骤3,直至【灰色集合】为空时结束,结束后仍在【白色集合】的对象即为GC Roots不可达,可以进行回收。
三色标级存在多标和漏标问题。
漏标
如图所示:
图中,有ABCD四个对象,A依赖B和C,C依赖D,初始标记完成后A对象已经被扫描过标为灰色,其他对象为白色;继续扫描B和C,当B和C扫描完后,A变成了黑色,B变成了灰色,C是黑色,D还是白色。此时如果应用程序将B和D的引用去掉,让C依赖D,建立起C和D的关系之后B变成了黑色。此时问题产生了,C已经是黑色,不会再对其依赖对象进行扫描,但事实上C还有一个依赖对象D没有被扫描。如果进行垃圾回收,D会被回收掉,这就是漏标问题。
漏标解决方案
增量更新:将新增的引用维护到一个集合中,将引用的源头变为灰色,等待重新标记阶段再重新进行一次扫描。 如:当D的引用指向了C,则会将C变为灰色,并将C放到一个新增引用的集合中,在重新标记阶段会将C作为根节开始继续向下扫描。
多标
如图所示:
图中,有ABCD四个对象,AB是黑色,C是灰色,D是白色,当GC正在扫描D时,B被置空了,此时B理应被回收,但是因为GC不会对黑色对象做重复扫描,所以B还是黑色,在进行垃圾清理时不会被回收,只能等下次GC时再进行重新标记扫描。这种情况相对于漏标来说不会导致系统出BUG。
并发预处理
并发预清理主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为Direty的Card )。
如图所示:
图中,节点3引用节点7,由于节点3的Card标记为Dirty,会将节点7标记为存活对象。
可终止预处理
可终止预处理阶段与并发预处理节点一样,主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为Direty的Card )。但是可终止预处理是有条件触发的,触发条件由CMS的两个参数控制:
- 参数CMSScheduleRemarkEdenSizeThreshold,默认值:2M。
- 参数CMSScheduleRemarkEdenPenetration,默认值:50%。
这两个参数一般是组合使用,即:当Eden空间使用超过2M时,启动可终止预处理,当Eden空间使用率到达50%时中断,进入重新标记阶段。
同时,CMS提供了一个参数CMSMaxAbortablePrecleanTime (默认为5S),表示不管Eden空间使用率是否到达参数CMSScheduleRemarkEdenPenetration配置的值,都会中断,进入重新标记阶段。
最后,CMS还提供参数CMSScavengeBeforeRemark(默认关闭,建议开启,开启方式:-XX:+CMSScavengeBeforeRemark),表示进入重新标记前强行执行一次Minor GC。
重新标记
重新标记主要是标记整个老年代的所有的存活对象,该阶段会扫描整个堆内存。扫描新生代的原因是因为老年代中的对象,如果被新生代中的对象引用,会被视为存活对象,即使新生代的对象已经不可达,也会使用这些不可达的对象当GC Root来扫描老年代。
重新标记阶段耗时较长,可以通过设置参数-XX:+CMSScavengeBeforeRemark在重新标记前先执行一次Minor GC,回收掉新生代中不可达对象,并将剩余对象转入幸存者区或晋升到老年代,这样在扫描新生代时,只需要扫描幸存者区对象即可,将大大减少扫描对象所需时长。
同时,可通过设置参数CMSParallelRemarkEnabled开启并行重新标记,提高标记效率,减少重新标记处理时长。
注意:重新标记阶段会引发STW。
并发清除
并发清除主要是清除那些没有被标记的对象,回收内存空间。
由于并发清理阶段应用程序仍在运行,因此,会继续产生新的不可达对象(即:垃圾),这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理这些不可达对象,需等待下一次GC时进行清理,这一部分垃圾称为浮动垃圾。
并发重置
并发重置主要是重置CMS的数据结构,准备在下一个CMS生命周期中使用。
该阶段与应用程序并发运行。
CMS问题及优化
CMS为何采用标记清除算法
因为CMS的垃圾回收阶段是并发回收的,如果使用标记整理算法,对象的内存地址会进行移动,为了避免因内存地址移动带来BUG,需要对用户线程的对象指针进行维护,这个过程会引发STW,同时,这样处理提高了垃圾清理时长(停顿时间也会增加),不符合CMS以最短回收停顿时间为目的设计的初衷。
如何降低重新标记停顿时长
一般CMS的GC耗时80%都在重新标记阶段,可以尝试添加参数-XX:+CMSScavengeBeforeRemark进行优化。
内存碎片如何优化
CMS采用标记-清除算法,回收过程会产生内存碎片。内存碎片过多时,会给大对象分配带来影响(如:老年代剩余空间足够,却没有足够的连续内存空间分配给大对象,从而触发Full GC)。
CMS提供了两个参数:
- UseCMSCompactAtFullCollection(默认开启)表示在要进行Full GC时,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。
- CMSFullGCsBeforeCompaction表示执行指定次数不压缩的Full GC后,执行一次带压缩的Full GC。默认值为0,表示每次进入Full GC时都进行碎片整理。
注意:CMSFullGCsBeforeCompaction参数虽然会降低Full GC压缩频率,减少停顿时长。但是会加剧内存碎片的产生,增加Full GC触发频率,因此,设置时需要在Full GC停顿时长和内存碎片数量之间做权衡。
Promotion Failed与Concurrent Mode Failure
Promotion Failed
Promotion Failed问题发生在Minor GC过程中,Survivor Space放不下转移的对象,老年代也放不(Promotion Failed发生的时候老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,下一步就会产生Concurrent Mode Failure,发生STW降级为Serial Old)。
Concurrent Mode Failure
Concurrent Mode Failure是CMS特有的错误,CMS的垃圾清理线程和用户线程并行进行的。老年代正在清理时,从年轻代晋升了新对象,或者新生代放不下分配的大对象直接在老年代分配内存,此时,如果老年代也放不下这些晋升对象或大对象,则会抛出Concurrent Mode Failure。
Concurrent Mode Failure的影响:老年代的垃圾收集器从CMS退化成Serial Old,所有用户线程被暂停,停顿时间变长。
解决方案
1)CMS触发太晚
参数-XX:CMSInitiatingOccupancyFraction=N指的是CMS在对内存占用率达到N%时开始GC(因为CMS会存在浮动垃圾,所以需要设置触发垃圾回收的阀值,默认为92%),可适当调小参数-XX:CMSInitiatingOccupancyFraction=N的值,如:-XX:CMSInitiatingOccupancyFraction=70。
2)空间碎片太多
开启空间碎片整理,并将空间碎片整理周期设置在合理范围。
如:-XX:CMSFullGCsBeforeCompaction=5,表示执行5次不压缩的Full GC后,执行一次带压缩的Full GC。
3)垃圾产生太快
- 晋升阈值太小。
- Survivor空间过小。
- Eden区过小导致晋升速率过快。
- 存在大对象。
CMS相关参数
-XX:+UseConcMarkSweepGC
打开CMS GC收集器。JVM在1.8之前默认使用的是Parallel GC,9以后使用G1 GC。
-XX:+UseParNewGC
当使用CMS收集器时,默认年轻代使用多线程并行执行垃圾回收(UseConcMarkSweepGC开启后则默认开启)。
-XX:+CMSParallelRemarkEnabled
采用并行标记方式降低停顿(默认开启)。
-XX:+CMSConcurrentMTEnabled
被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。(默认开启)
-XX:ConcGCThreads
定义并发CMS过程运行时的线程数。
-XX:ParallelGCThreads
定义CMS过程并行收集的线程数。
-XX:CMSInitiatingOccupancyFraction
该值代表老年代堆空间的使用率,默认值为92。当老年代使用率达到此值之后,并行收集器便开始进行垃圾收集,该参数需要配合UseCMSInitiatingOccupancyOnly一起使用,单独设置无效。
-XX:+UseCMSInitiatingOccupancyOnly
该参数启用后,参数CMSInitiatingOccupancyFraction才会生效。默认关闭。
-XX:+CMSClassUnloadingEnabled
相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置-XX:+CMSClassUnloadingEnabled。默认关闭。
-XX:+CMSIncrementalMode
开启CMS收集器的增量模式。增量模式使得回收过程更长,但是暂停时间往往更短。默认关闭。
-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理,默认值0。
-XX:+CMSScavengeBeforeRemark
在cms gc remark之前做一次ygc,减少gc roots扫描的对象数,从而提高remark的效率,默认关闭。
-XX:+ExplicitGCInvokesConcurrent
该参数启用后JVM无论什么时候调用系统GC,都执行CMS GC,而不是Full GC。
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
该参数保证当有系统GC调用时,永久代也被包括进CMS垃圾回收的范围内。
-XX:+DisableExplicitGC
该参数将使JVM完全忽略系统的GC调用(不管使用的收集器是什么类型)。
-XX:+UseCompressedOops
这个参数用于对类对象数据进行压缩处理,提高内存利用率(默认开启)。
-XX:MaxGCPauseMillis=200
这个参数用于设置GC暂停等待时间,单位为毫秒,不要设置过低。