CMS设计的目的是尽可能的降低STW时间,但是CMS有很多问题,比如用标记清除算法,会产生很多的内存碎片,比如为了减少STW时间,采用的并发标记和并发清除,会在每次GC产生一些不可清理的内存,加上之前内存碎片,给人的感觉千疮百孔,于是就有很多针对这些问题而生的jvm参数。CMS在jdk9已经标位Deprecate,不推荐使用了。
CMS是一个老年代的收集器,经常配合使用的是ParNew,在JDK9之前还可以和Serial一起使用,但是JDK9之后就不能一起用了,别问我为啥,我也不知道。
CMS面试最常见的问题是标记清除的四个步骤是什么。
1.初始标记,(STW)(只扫描GCroot直接对应的对象)
2.并发标记,
3.重新标记,(STW)(遍历整个GCroot)
4.并发清除,
对应gc日志里,这四个步骤还会分为具体的7个步骤,并发标记之后还会有CMS-concurrent-preclean/Rescan (parallel) 和CMS-concurrent-abortable-preclean,
CMS-concurrent-preclean/Rescan (parallel) 并发预清理阶段,也是一个并发执行的阶段。在本阶段,会查找前一阶段执行过程中,从新生代晋升或新分配或被更新的对象。通过并发地重新扫描这些对象,预清理阶段可以减少下一个stop-the-world 重新标记阶段的工作量。
CMS-concurrent-abortable-preclean 并发可中止的预清理阶段。这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个STW重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段。
最后还会有一个重置的步骤。
最耗时的两个步骤是并发标记和并发清除,因为系统会分出部分线程给用户线程,部分线程分给收集器。这个公式默认是回收线程数=(CPU核心数+3)/4,也就是说当核心数特别大的时候只会分出1/4的线程来进行GC,就会特别的慢,而核心数特别少的时候,回收线程数就会占据大量核心,导致用户线程变得特别慢。
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
由于上文说的,并发清除在清除时,用户线程还在进行,会产生新的对象,所以很有可能在并发清除结束之前,内存就已经溢出了,所以上边这两个参数是应付这件事的,第一个参数描述是如果不开启,CMS第一次GC会按照第二个参数的值进行,之后则自己评估,而下边这个值是当内存占比达到70%这个阈值的时候则会开始CMS的GC过程,这个值过小会导致频繁GC,过大可能会导致内存溢出,这种情况出现的时候CMS则会转为SerialOld来执行GC,
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=10
上边这两个参数也是一起使用的,上边这个参数开启了,下边才有用,大概意思是在执行10次GC后会在下次gc之前整理空间。