JVM-CMS垃圾收集器

简介

全称Concurrent Mark Sweep,以牺牲吞吐量为代价来获得最短停顿时间的垃圾收集器,适用于对响应时间的侧重性大于吞吐量的场景。CMS仅针对老年代(Old Generation)的回收,减少full gc发生的几率。默认情况下在老年代使用了68%及以上的内存的时候就开始进行CMS。并发标记和并发清除阶段的GC线程是和用户线程并发执行,默认GC线程数为物理CPU核心数的1/4。在使用CMS垃圾收集器的情况下,对于年轻代的回收,是用并行垃圾收集器(ParNew GC)去完成的。

工作周期

初始标记(Initial Mark)

这个过程会暂停所有的应用程序线程,只运行GC线程,因此会发生STW(stop the world)事件。

整个过程需要标记的对象有:

  • 老年代中所有的GC Roots所指的直接对象
  • 被活着年轻代中的对象引用老年代的对象(引用的对象指老年代中的对象)

Initial Mark

因为标记的对象比较少,所以暂停时间会比较短

并发标记(Concurrent Mark)

在初始标记阶段中所标记的节点往下检索,标记出所有老年代中存活的对象。该过程会和应用程序线程并发地执行,不会发生停顿。注意此时会有部分对象的引用被改变。
Concurrent Mark

并发预清理(Concurrent Preclean)

也是一个并发阶段,会和应用程序线程并发地执行。前一个阶段在并行运行的时候,一些对象的引用已经发生了变化,当这些引用发生变化的时候,JVM会标记堆的这个区域为Dirty Card(包含被标记但是改变了的对象,被认为“dirty”),这就是 Card Marking
Concurrent Mark1
pre-clean阶段,那些能够从dirty card对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了
Concurrent Mark2

可中止的并发预清理(Concurrent Abortable Preclean)

又一个并发阶段不会停止应用程序线程。这个阶段尝试着去承担STW的Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。

重新标记(Final Remark)

这个阶段是CMS中第二个并且是最后一个STW的阶段。该阶段的任务是完成标记整个老年代的所有的存活对象,包括重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),所以STW时间会比第一阶段的长
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。

并发清理(Concurrent Sweep)

和应用程序线程同时进行,不需要STW。这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。
Concurrent Sweep

并发标记重置(concurrent reset)

重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用,与应用程序线程并发地执行。

使用方式

  • -XX:+UseConcMarkSweepGC ,启用CMS,同时-XX:+UseParNewGC会被自动打开。
  • CMS默认启动的回收线程数目是 (ParallelGCThreads + 3)/4) ,如果你需要明确设定,可以通过-XX:ParallelCMSThreads=20来设定,其中ParallelGCThreads是年轻代的并行收集线程数
  • 年轻代的并行收集线程数默认是(cpu <= 8) ? cpu : 3 + ((cpu * 5) / 8),如果你希望降低这个线程数,可以通过-XX:ParallelGCThreads= N 来调整。
  • -XX:CMSInitiatingOccupancyFraction,设置第一次启动CMS的阈值,默认是68%
  • CMS收集器默认不会对永久代进行垃圾回收(在1.7中是默认关闭,但是在1.8中是默认打开的),为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:
    +CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
  • -XX:+CMSScavengeBeforeRemark,强制remark之前开始一次minor GC,可以减少remark的等待时间,因为老年代的对象有的会依赖于新生代的对象,当增加了这个命令时会在remark之前执行一次minor GC的操作,从而可以减少老生代到新生代的可到达对象数。默认为false。
  • -XX:UseCMSCompactAtFullCollection来启动整理堆碎片,启动这个功能后,默认每次执行Full GC的时候会进行整理(也可以通过-XX:CMSFullGCsBeforeCompaction=n来制定多少次Full GC之后来执行整理),整理碎片会stop-the-world

完整例子

1
2
3
4
 -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=64m 
-XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled
-XX:SoftRefLRUPolicyMSPerMB=0 -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

CMS优缺点

优点

  • 并发收集
  • 低停顿

缺点

  • CMS收集器对CPU资源非常敏感。在并发处理阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
  • CMS是一款“标记-清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”
标记-清除算法:从根集合开始扫描,对存活的对象进行标记,比较完毕后,再扫描整个空间中未标记的对象,然后将没有标记的对象全部清除掉,不需要对象进行移动

Full GC

CMS 进行垃圾回收时可能会发生Full GC的情况:

  • prommotion failed
    原因:存活区(surivivor)空间不足,对象进入老年代,而此时老年代没有空间容纳对象,将导致一次 Full GC
    解决方式:调整-XX:SurvivorRatio参数,这个参数是Eden区和Survivor区的大小比值,默认是8:1:1。调小这个参数增大survivor区空间,让对象尽量在survivor区呆长一点,减少进入老年代的对象。
  • concurrent mode failed
    原因:CMS 回收速度慢,CMS 完成前,老年代已被占满,将导致一次 Full GC
    解决方式:调小-XX:CMSInitiatingOccupancyFraction参数的值,让CMS更早更频繁的触发,降低老年代被占满的可能。