介绍
CMS垃圾回收器的全称是Concurrent Mark-Sweep Collector,从名字上可以看出两点,一个是使用的是并发收集,第二个是使用的收集算法是Mark-Sweep。从而也可以推测出该收集器的特点是低延迟并且会有浮动垃圾的问题。下面详细介绍一下这个收集器的特点。
CMS收集器
CMS收集器是为了低延迟而生,通过尽可能的并行执行垃圾回收的几个阶段来把延迟控制到最低。CMS收集器是老年代的垃圾收集器,一般情况下会有ParNew来配合执行(默认情况下也是ParNew),ParNew也是使用并行的算法来执行年轻代的回收。当然除此之外,你还可以选择使用Serial收集器来收集年轻代,不过一般很少这样使用。通过咱们说的CMS收集器是指广义上的CMS收集器,包含以下几个:ParNew(Young)GC + CMS(Old)GC + Serial GC 算法(应对核心的CMS GC某些时候的不赶趟,开销很大)。本文重点介绍一些CMS在Old区域的回收。
触发条件
CMS垃圾收集器的触发条件有以下几个:
1、如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(建议带上这个参数)。
2、老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%,前提是配置了第一个参数。
3、永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled并且配置了第一个参数。
4、新生代的晋升担保失败。
CMS的收集阶段
CMS收集器在收集老年代的时候分为以下几个阶段:初始标记、并发标记、预清理、可中断预清理、最终标记、并发清除、并发重置。每个阶段的运行过程如下:
下面分别从这几个阶段来介绍CMS收集器。
初始标记
初始标记阶段主要做两件事:一是遍历GCRoot可直达的老年代对象;二是遍历新生代直达的老年代对象。这里的直达是指直接关联到GCRoot的一级对象。初始标记阶段是完全STW的,引用程序会暂停。通过-XX:+CMSParallelInitialMarkEnabled参数可以开启该阶段的并行标记,使用多个线程进行标记,减少暂停时间。
哪些对象可以作为GCRoot:
1、所有Java线程当前栈帧引用的,也就是正在被调用的方法的引用类型的参数、局部变量以及临时值。
2、所有的静态数据结构引用的对象
3、String常量池里的引用
4、运行时常量池里引用的类型
并发标记
并发标记阶段是与应用程序一起执行的,这个阶段主要做两件事:
1、对初始标记中标记的存活对象进行trace,标记这些对象为可达对象,例如A->B,A在初始标记被识别,而B就是在并发标记阶段被识别。
2、将在并发阶段新生代晋升到老年代的对象、直接在老年代分配的对象以及老年代引用关系发生变化的对象所在的card标记为dirty,避免在重新标记阶段扫描整个老年代。
因为并发标记阶段与引用程序一起执行,因此会出现之前A->B->C变成A->C的情况,这种情况下C对象时无法在并发标记阶段被标记的。在标记阶段会使用三色标记算法。
三色标记法把 GC 中的对象划分成三种情况:
白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
灰色:正在搜索的对象
黑色:搜索完成的对象(不会当成垃圾对象,不会被 GC)
假设刚开始的对象图如下:
在并发标记阶段,B对象引用C对象变为A对象引用C对象,如下
这时候再扫描的时候就会变成如下的图
此时C对象变成了白色的,但是显然是不正确的,于是就有两种方式来处理这种情况:
1、在对象引用发生变化之前记录对象引用关系
2、在对象引用发生变化之后记录对象引用关系
这两种思路正好对应了CMS和G1的两种处理方式。在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象
预清理
通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情
1、并发标记阶段在Eden分配了对象A,并且A引用了老年代对象B,那么这个阶段标记B为活跃对象
2、扫描并发标记阶段的Dirty Card,重新标记那些在并发标记阶段引用被更新的对象
可中断预清理
该阶段存在的目的是减轻重新标记的工作量,减少暂停时间,主要做两件事情:
1、扫描处理DirtyCard中的对象
2、处理新生代引用到的老年代的对象
该阶段退出的条件有三个:
1、CMSMaxAbortablePrecleanTime参数控制的5秒退出
2、Eden区达到CMSScheduleRemarkEdenPenetration参数配置的值(默认50%)
3、CMSMaxAbortablePrecleanLoops控制的扫描次数(默认是0,不退出)
该阶段是希望能发生一次Young GC,这样就可以减少Eden区对象的数量,降低重新标记的工作量,因为重新标记会扫描整个Eden区的
最终标记
最终标记又叫重新标记,该阶段也是STW的,主要会遍历三个地方:
1、遍历Eden区,重新标记
2、遍历DirtyCard,重新标记
3、遍历GC Root,重新标记
由于该阶段遍历的区域很多,因此有可能会耗时比较长,并且该阶段是完全的STW的。
通过CMSScavengeBeforeRemark参数可以强制在重新标记阶段之前强制进行一次YoungGC,通过设置CMSParallelRemarkEnabled参数可以开启并行的Remark,加快remark的速度。
并发清理
移除那些不用的对象,回收他们占用的空间并且为将来使用。该阶段有可能产生浮动垃圾,可以通过参数UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction来控制压缩次数。
并发重置
该阶段是最后一个阶段,重置CMS的数据结构。
CMS日志说明
这是一段完整的CMS日志:
- 2018-04-28T01:18:00.485+0800: 30991.893: [GC (CMS Initial Mark) [1 CMS-initial-mark: 845243K(1228800K)] 893692K(2611200K), 0.0195530 secs] [Times: use
- r=0.04 sys=0.00, real=0.02 secs]
- 2018-04-28T01:18:00.505+0800: 30991.913: [CMS-concurrent-mark-start]
- 2018-04-28T01:18:00.944+0800: 30992.353: [CMS-concurrent-mark: 0.429/0.439 secs] [Times: user=0.94 sys=0.00, real=0.44 secs]
- 2018-04-28T01:18:00.944+0800: 30992.353: [CMS-concurrent-preclean-start]
- 2018-04-28T01:18:00.953+0800: 30992.362: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
- 2018-04-28T01:18:00.953+0800: 30992.362: [CMS-concurrent-abortable-preclean-start]
- CMS: abort preclean due to time 2018-04-28T01:18:06.050+0800: 30997.459: [CMS-concurrent-abortable-preclean: 5.089/5.097 secs] [Times: user=5.92 sys=
- 0.17, real=5.09 secs]
- 2018-04-28T01:18:06.053+0800: 30997.462: [GC (CMS Final Remark) [YG occupancy: 222099 K (1382400 K)]30997.462: [Rescan (parallel) , 0.0285059 secs]309
- 97.491: [weak refs processing, 0.0334976 secs]30997.524: [class unloading, 0.0806913 secs]30997.605: [scrub symbol table, 0.0359491 secs]30997.641: [s
- crub string table, 0.0035671 secs][1 CMS-remark: 845243K(1228800K)] 1067342K(2611200K), 0.1878564 secs] [Times: user=0.38 sys=0.00, real=0.18 secs]
- 2018-04-28T01:18:06.242+0800: 30997.650: [CMS-concurrent-sweep-start]
- 2018-04-28T01:18:06.659+0800: 30998.068: [CMS-concurrent-sweep: 0.417/0.417 secs] [Times: user=0.55 sys=0.00, real=0.42 secs]
- 2018-04-28T01:18:06.660+0800: 30998.069: [CMS-concurrent-reset-start]
- 2018-04-28T01:18:06.663+0800: 30998.072: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
这段日志展示了CMS回收时的各个阶段,在CMS Initial Mark阶段,老年代容量是1228800K,目前使用了845243K,该阶段耗时0.0195530秒,这段时间是STW的;接下来进行concurrent-mark阶段,耗时0.429秒;preClean阶段耗时0.009秒;再往下是abortable-preclean阶段,这个阶段耗时5秒,结束的原因是最长配置的该阶段运行时间是5s,这个阶段并没有发生YoungGC;接下来是第二个STW,Final Remark阶段,222099 K (1382400 K)分别是年轻代的占用和总空间情况,该阶段还会进行弱引用的处理、class卸载以及符号表的扫描,845243K(1228800K)是老年代的使用量和总空间,这个阶段总共耗时0.1878564秒;标记完成之后就开始concurrent-sweep阶段进行并发清除,然后运行concurrent-reset重置CMS数据结构。
CMS失败处理
在运行CMS收集器的时候,可能会出现两种类型的失败
并发模式失败日志(concurrent mode failure)
当老年代无法容纳新生代GC晋升的对象时发生并发模式失败,并发模式失败意味着CMS退化成完全STW的Full GC,也就是Serial GC。下面的图片揭示了在发生并发模式失败时的日志:
针对这种情况,有两个方面需要考虑:
1、给后台线程更多的运行机会。也就是说更早的启动并发收集周期。CMS收集器在老年代使用占到60%的时候启动比占到70%才启动,显然前者完成垃圾回收的几率更大。为了实现这种配置,可以同时设置以下两个参数:-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly。如果同时设置了这两个参数就可以让CMS只根据老年代的使用比例来决定是否启动CMS垃圾收集。
2、更多的线程来运行CMS。之所以出现并发模式失败,是因为CMS的速度跑不赢对象晋升到老年代的速度了。所以可以通过给CMS更多的线程来加快CMS的速度。可以通过-XX:ConGCThreads=N来设置后台线程的数量。默认情况下线程数ConcGCThreads=(3+ParallelGCThreads)/4,是根据ParallelGCThreads来计算的,ParallelGCThreads的值可以通过-XX:ParallelGCThreads参数来设置。并不是说设置越多的线程来运行CMS越好,因为CMS在运行的时候会完整的占用一颗CPU,所以在CPU比较紧张的情况下,这个值还是要谨慎设置的。
晋升失败(promoration failure)
老年代有足够的空间,但是由于碎片化严重,无法容纳新生代中晋升的对象,发生晋升失败。下面的图片揭示了在发生晋升失败时的日志:
晋升失败的原因是碎片化严重,所以这个问题的解决方案就是如何减少碎片化的问题。CMS提供了两个参数来对碎片进行整理和压缩。-XX:+UseCMSCompactAtFullCollection这个设置的作用是在进行FullGC的时候对碎片进行整理和压缩。-XX:CMSFullGCsBeforeCompaction=*这个参数是设置在进行多少次FullGC的时候对老年代的内存进行一次碎片整理压缩。通过设置这两个参数可以有效的对碎片问题进行优化。同样需要注意的是对碎片进行整理压缩是一个比较耗时的操作,所以也需要谨慎设置。
CMS的一些参数说明
结合上面已经讲过的参数配置,下面给出CMS的一些参数说明:
-XX:+UseConcMarkSweepGC 激活CMS收集器
-XX:ConcGCThreads 设置CMS线程的数量
-XX:+UseCMSInitiatingOccupancyOnly 只根据老年代使用比例来决定是否进行CMS
-XX:CMSInitiatingOccupancyFraction 设置触发CMS老年代回收的内存使用率占比
-XX:+CMSParallelRemarkEnabled 并行运行最终标记阶段,加快最终标记的速度
-XX:+UseCMSCompactAtFullCollection 每次触发CMS Full GC的时候都整理一次碎片
-XX:CMSFullGCsBeforeCompaction=* 经过几次CMS Full GC的时候整理一次碎片
-XX:+CMSClassUnloadingEnabled 让CMS可以收集永久带,默认不会收集
-XX:+CMSScavengeBeforeRemark 最终标记之前强制进行一个Minor GC
-XX:+ExplicitGCInvokesConcurrent 当调用System.gc()的时候,执行并行gc,只有在CMS或者G1下该参数才有效
----------------------------------------------------------------------------------------------
欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。