CMS(Concurrent Mark Sweep)垃圾收集器

本篇博客主要讲解了CMS(Concurrent Mark Sweep)垃圾收集器工作原理

CMS过程

  1. 初始标记(STW initial mark)
  2. 并发标记(Concurrent marking)
  3. 并发预处理(Concurrent precleaning)
  4. 可中断并发预清理(Concurrent AbortablePreclean)
  5. 重新标记(STW remark)
  6. 并发清理(Concurrent sweeping)
  7. 并发重置(Concurrent reset)

CMS流程图

初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的”根对象”开始,只扫描到能够和”根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为重新标记阶段会Stop The World。

可中断并发预清理:该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。在该阶段,主要循环的做两件事:处理 From 和 To 区的对象,标记可达的老年代对象以及和上一个阶段一样,扫描处理Dirty Card中的对象。

重新标记:这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从”跟对象”开始向下追溯,并处理对象关联。

并发清理:清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

并发重置:这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS 的缺点

1) CMS 回收器采用的基础算法是 Mark-Sweep。所以 CMS 不会整理、压缩堆空间。这样就会有一个问题:经过 CMS 收集的堆会产生空间碎片。 CMS 不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS 回收器不再采用简单的指针指向一块可用堆空间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当 JVM 分配对象空间的时候,会搜索这个列表找到足够大的空间来 hold 住这个对象。
优化:可开启 -XX:+UserCMSCompactAtFullGonllection 开关(默认就是开启的)来使用 Serial Old 这个标记-整理算法的收集器进行回收,通过 -XX:CMSFullGCsBeforeCompaction 参数可以设置执行多少次不压缩的 Full GC 之后执行一次带压缩的 GC(默认是0,表示每次都压缩)。

2) 需要更多的 CPU 资源。从上面的图可以看到,为了让应用程序不停顿,CMS 线程和应用程序线程并发执行,这样就需要有更多的 CPU,单纯靠线程切换是不靠谱的。并且,重新标记阶段,为空保证 STW 快速完成,也要用到更多的甚至所有的CPU资源。

3) CMS 的另一个缺点是它需要更大的堆空间。因为 CMS 标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在 CMS 回收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS 不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,以避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用 68%(JDK1.6之后为92%)的时候,CMS 就开始行动了。 – XX:CMSInitiatingOccupancyFraction = n 来设置这个阀值。另外,如果 CMS 运行期间预留的内存无法满足程序的需要,就会出现一次 “Concurrent Mode Failure” 失败,这时虚拟机会启动后备方案,临时启用 serial Old 收集器来重新收集老年代的垃圾,这时的停顿就比较长了。

总得来说,CMS 回收器减少了回收的停顿时间,但是降低了堆空间的利用率。

主动GC(foreground collector) 和 周期性GC(background collector)

1)主动GC: 触发条件比较简单,一般是遇到对象分配但空间不够,就会直接触发 GC,来立即进行空间回收。因为必须要分配完内存之后业务才能继续运行,因此需要进行 STW,因此也不会经历 CMS 的 2、3、4 这三个阶段,采用的算法是 mark sweep,不压缩。

2)周期性GC:在GC中比较常见,由后台线程 ConcurrentMarkSweepThread 循环判断(默认2s)是否需要触发。触发条件如下:

  1. 如果没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议带上这个参数,不然会加大问题排查的难度)
  2. 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
  3. 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
  4. 新生代的晋升担保失败

注意:system.gc 默认使用主动GC,因此如果需要调用 system.gc 来进行GC(例如使用RMI/NIO大量使用了堆外内存),最好开启 -XX:+ExplicitGCInvokesConcurrent 参数来优化GC,此时会使用周期性GC。当然,频繁使用堆外内存也可通过 -XX:MaxDirectMemorySize 来指定最大堆外内存,当使用达到了阈值调用 system.gc 来做一次Full GC。

附录:各个收集器搭配关系图

垃圾收集器可搭配图