JVM Garbage Collector——CMS

CMS定义

CMS的全称是Concurrent Mark Sweep。

从名称上可以看出CMS的特点,可并发,使用标记-清除算法。CMS是针对老年代垃圾回收的收集器

CMS目标

CMS设计时的目标就是获取最小的停顿时间,也就是低延迟。对于响应时间比较敏感的应用比较适合使用CMS

CMS过程


CMS的执行过程可以分为以下几个阶段

  1. Initial Mark(STW)
  2. Concurrent Mark
  3. Concurrent Preclean
  4. Final Remark(STW)
  5. Concurrent Sweep
  6. Reset

Initial Mark(初始标记)

在这个阶段会进行可达性分析,因此需要Stop the world,暂停应用程序。

初始标记仅仅标记两类对象,因此速度很多,STW的时间很短

  1. GC ROOT直接关联的对象
  2. 新生代引用的老年代对象

Concurrent Mark(并发标记)

在这个阶段GC线程与用户并发进行,从初始标记阶段标记的对象出发,标记所有可达的对象。

Concurrent Preclean(并发预清理)

由于Concurrent Mark阶段是应用线程和GC线程并发执行的,那么在这个期间,可能会有对象从新生代晋升到老年代,也有一些引用会发生改变,所有在这个阶段会标记新生代晋升的对象新分配到老年代的对象以及在并发阶段被修改了的对象

我们知道CMS的目标是获取最短的停顿时间,如果不在这个阶段进行这些标记,那么下一个remark阶段就需要更多的工作,而这个阶段是需要STW,因此要把一些复杂的操作在预清理阶段完成。

归根结底,在这个阶段主要处理一个问题

  • 如何确定老年代的对象是活着的

答案也很简单,通过GC ROOT可达的对象就是活着的,因此需要扫描新生代和老年代。但是全量扫描新生代和老年代肯定会非常耗时,因此需要一个能够快速识别新生代和老年代活着对象的机制。

新生代识别活着对象

对于新生代来说,经过一次Young GC后剩下的对象肯定都是活着的,并且活着的对象很少。

可想而知,如果在扫描新生代之前发生一次Young GC,那么扫描的效率将大大提高。

CMS有两个参数:

  1. CMSScheduleRemarkEdenSizeThreshold 默认2M
  2. CMSScheduleRemarkEdenPenetration 默认值50%

这两个参数的意思,在Eden空间的大小超过2M时,启动可中断的并发预清理(CMS-concurrent-abortable-preclean), 直到Eden的空间利用率超过50%时中断,进入到下一个阶段(remark)。

可中断的并发预清理是为了等待一次Young GC的发生,但是我们知道这个是不可控的,因此需要控制这个阶段的执行时间,CMS通过以下两个参数来控制

  1. CMSMaxAbortablePrecleanLoops 可中断的并发预清理的执行次数超过这个值,默认是0
  2. CMSMaxAbortablePrecleanTime 执行可中断的并发预清理的时间超过这个值,默认是5S

另外CMS还提供了CMSScavengeBeforeRemark参数,使在进入remark阶段之前强制执行一次Young GC。

老年代识别活着对象

老年代会维护一个叫CARD TABLE的数组,数组中每个位置存的是个byte,CMS将老年代的空间分成512bytes的块,card table中的每一个元素对应一块。

在并发标记阶段,如果某个对象的引用发生了变化,那么就标记这个对象所在的块为dirty card。

在并发预清理阶段就会重新扫描这个块,将该对象引用的对象标记为可达。

card table还有一个作用,如果一个老年代对象引用了新生代的对象,那么它对应的块也会被标记为dirty card,这样在Young GC阶段通过扫描card table就可以快速识别老年代引用的对象

Final Remark(重新标记)

暂停所有应用线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。注意这个阶段是多线程

Concurrent Sweep(并发清理)

应用线程被激活,同时清理哪些无效的对象

Reset(重置)

CMS清除内部状态,为下次回收做准备

CMS存在的问题

1. 抢占CPU资源

CMS是并发的,而并发就意味着CPU资源,即GC线程与应用线程抢占CPU,这样可能会造成应用执行效率下降。

CMS默认的回收线程数是(CPU个数+3)/4,可以看到如果CPU个数为2,CMS会启动一个GC线程,相当于GC线程占用了50%的CPU资源。

但是对于目前的场景来说,PC至少都是双核处理器,更别说大型的服务器了。

2. Concurrent Mode Failure

由于并发清理阶段,用户线程还在运行,所以必须预留出一定的空间提供给用户线程,不能像其他收集器那样等到老年代满了在进行GC。

CMS提供了CMSInitiatingOccupancyFraction参数来设置老年代空间使用百分比,达到了百分比就进行垃圾回收,默认值是92%。

可以想象,如果这个参数设置过小,那么就会导致频繁的GC,如果设置的过高呢?假设将参数设置为99%,若用户线程所需的空间大于1%,那么就会产生Concurrent Mode Failure,意思是并发模式失败了。

这时,虚拟机就会启动备案:使用Serial Old收集器重新对老年代进行垃圾回收.如此一来,停顿时间变得更长

CMS还提供了动态检测机制,可以根据历史记录,来预测老年代还要多久填满以及进行一次回收所需的时间。这个特性可用通过UseCMSInitiatingOccupancyOnly来关闭

3. Floating Garbage

在并发清理阶段,用户线程还在运行,那么就有可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次GC处理,这些垃圾就成为Floating Garbage(浮动垃圾)

4. 空间碎片

CMS使用的是mark-sweep算法,可能会造成大量的空间碎片,空间碎片过多,会导致无法分配大对象,此时就不得不进行一次full gc

CMS的解决方案是使用UseCMSCompactAtFullCollection参数(默认开启),在顶不住要进行Full GC时开启内存碎片整理

同时还有另外一个参数CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,每次进入Full GC时都进行碎片整理)。

相关参数

参数 默认值 说明
UseConcMarkSweepGC 使用CMS
CMSScheduleRemarkEdenSizeThreshold 2M Eden空间大于2M,则开启可中断并发预清理
CMSScheduleRemarkEdenPenetration 50% Eden空间利用率大于50%,则中断预清理,进入remark
CMSMaxAbortablePrecleanLoops 0 执行可中断预清理的次数超过阈值,则进入remark
CMSMaxAbortablePrecleanTime 5S 执行可中断预清理的时间超过5秒,则进入remark
CMSInitiatingOccupancyFraction 92% 老年代空间利用率达到92%,则进行GC
UseCMSInitiatingOccupancyOnly true 是否开启自动预测GC时机
UseCMSCompactAtFullCollection true 是否在full gc时进行碎片整理
CMSFullGCsBeforeCompaction 0 执行几次不压缩的full gc后,执行一次带压缩的full gc

参考文献


Reprint please specify: wbl JVM Garbage Collector——CMS