概述
CMS(Concurrent mark sweep)是一种并发标记清理收集器.其主要目标是降低应用停顿时间
CMS收集器有3种基本操作
- 回收清理新生代(此时所有的应用线程都会暂停)
- 启动一个并发线程回收清理老年代
- 一般情况下CMS不会清理永生代,但CMS本身可能触发FullGC
CMS的设计思路是在GC的大多数时间保持与应用程序的并行,而只在很少的时间里要求串行STW,从而有效的降低应用停顿
CMS收集器的GC周期由6个阶段组成,其中需要两次STW
- 初始化标记(CMS Initial Mark,STW_01)
开始标记根对象,在CMS中根对象需要STW,第一次STW - 并发标记(CMS-concurrent-mark-start、CMS-concurrent-mark)
从根对象开始,遍历其引用,这是并行操作.(第一次并发遍历) - 并发预清理(CMS-concurrent-preclean-start、CMS-concurrent-preclean)
标记在第二阶段(并发标记)过程中,发生改变的对象再执行遍历.(第二次并发遍历) - 重新标记(CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,STW_02)
事实上在第二次并发遍历过程中,还会继续发生改变对象.不过因为第二次的对象是来自第一次过程中的对象,其数量相对较少.所以第二次遍历的对象也相对很少.这时就没必要来第三次遍历了,此时的操作是STW,然后更新清理 - 并发清理(CMS-concurrent-sweep-start、CMS-concurrent-sweep)
对最终标记对象进行清理回收.此时也是并发执行 - 并发重置(CMS-concurrent-reset-start、CMS-concurrent-reset)
收集器的一些收尾工作,让下一次GC启动之前有一个干净的环境,这里我们一般不关心.
CMS常见问题
CMS的问题主要有两个
- 内存碎片
从CMS的过程机制可以看出.CMS默认是标记清理算法,而不是标记整理算法,也就是没有内存整理环节,所以在未来某个时间必然会出现堆对象分配不多但就是无法分配出对象的情况.这是因为堆中充满了碎片
此时只能被迫触发FullGC,并且这是CMS的设计问题,也就是说此时的FullGC是无法避免的
当然更好的做法是设定在一定的次数的标记清理后,强制使用标记整理 - 并发模式失败(或晋升失败)
CMS的设计的并发遍历是不需STW的(这也是CMS的特性之一),也就是CMS并发遍历中,应用程序是在继续工作的.继续工作的本质是,是会继续产生对象(这些对象有一个专用名词叫浮动对象).
所以CMS的设计机制是,对象产生的频次和大小不会太高.至少是在CMS处理释放之前,堆不能直接占满被爆了.这种情况下,CMS会直接启动FullGC 并发模式失败(或晋升失败),不是CMS特有问题,包括G1,事实上每一个并发收集器都会面对这个问题.这也是CMS调优的一个重点:尽可能避免并发模式失败(或晋升失败)
晋升失败
晋升失败是是年轻代无法升入老年代中
此时GC日志会出现
....
[GC 106.641: [ParNew (promotion failed).....
解决办法
- 提前晋升
- 加大救助空间
并发模式失败
默认情况下,CMS将在使用老年代使用70%时开始启动.
也就是说CMS必须在剩余30%用完之前完成整个回收释放工作,一旦来不及完成就会产生并发模式失败引起FullGC
此时GC日志出现
concurrent mode failure
解决办法
- 增大老年代空间
- 提前触发GC
- 增加GC线程,让GC过程完成的更快
- 使用标记整理算法
这适用于内存碎片情况.因为CMS默认标记清除算法.所以无论优化工作做的多好都必然会产生内存碎片,并最终充斥整个堆从而最终分配不了任何对象.
CMS这里最好使用在一定标记清除之后强制使用一次标记整理,可以相当程度的缓解碎片问题
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
常用参数调优
-XX:+UseConcMarkSweep
启用CMS回收器.CMS不是JVM的默认GC(1.7,1.8都是并行,1.9是G1),使用CMS需要显式启用
-XX:UseParNewGC
激活年轻代使用多线程垃圾回收
默认情况下,CMS对年轻代和老年代是两种不同的垃圾算法.年轻代是串行,老年代是并发的.所以如果想对年轻代也是用多线程GC,需要显式启动.(高版本JVM已改成默认年轻代开启,最好是打印下参数看看是否已经开启了)
-XX:+CMSConcurrentMTEnabled
激活CMS的并发阶段使用多线程,这个是开启CMS默认开启.
-XX:ConcGCThreads=N
定义CMS并发阶段可以使用的线程数
更多的GC线程会加快回收速度,但会带来更多的同步开销.所以事实上效果如何必须严格测试
总的来说,GC线程数调优优先级相当靠后,一般都是用默认,并且在其它调优明显无效的情况下才会最后考虑调整线程数
-XX:CMSInitiatingOccupancyFraction
CMS的收集器必然是在老年代满之前启动的,并且必须尽可能的保证在老年代真正满前完成GC
该参数就是控制老年代使用到什么地步时开始CMS收集的阈值,默认80%
调低可以显著避免并发失败导致的FullGC,但也会更加频繁的触发GC,因为这相当于浪费更多的堆空间
这个配置还有个配套参数 -XX:+UseCMSInitiatingOccupancyOnly
是否强制使用堆阈值.
通过CMSInitiatingOccupancyFraction设定80%,则必然在老年代使用达到80%是开始GC
但如果没有开启该参数时,在一个小于80%的时候,如果CMS认为有必要仍然会启动GC
如果开启了该参数,则CMS必须等待堆阈值达到才会触发GC而不会自行其是(一般其实没有开,除非特别情况,否则CMS认为应该启动想必是有一定道理的,一个没满的GC也不见得会耗用多少时间,这个看如果这种自行其是确实干扰到应用程序了,可以关掉它)
-XX:CMSMaxAbortablePrecleanTime=N
-XX:+CMSClassUnloadingEnabled
在Java7中,CMS默认不受理永生代.如果希望CMS同时接管永生代GC,可以开启此参数
在Java8中,开启CMS已经默认开启该参数了
如果应用程序中存在大量的自定义运行时加载,建议开启该参数对不再使用的类元数据卸载
使用自定义运行时加载时,务必要注意回收和释放,事实上,方法区(永生代或者元空间)比一般意义上的年轻代老年代要脆弱的多
-XX:+DisableExplicitGC
这是一个非只在CMS使用的参数,但一般都会设置
这个参数的意义非常简单,让应用程序中的System.gc无效
-XX:+UseCMSCompactAtFullCollection
这是开启内存整理,并在内存清理算法之后,跟一次内存整理算法
这是一个很好的解决CMS碎片问题的方法.但必须说明的是,内存整理的那一次GC会明显耗时更长