CMS


​ 主要分析了CMS收集器的算法实现和收集流程,和部分关键参数对CMS的影响,以及三色标记如何解决对象漏标问题。并在最后总结了CMS的优缺点

​ CMS(Concurrent Mark-Sweep)是一种基于标记-清除算法实现的老年代垃圾回收器,以获取最短停顿时间为目标,适合对响应时间敏感的应用(如 Web 系统)。其核心思想是尽可能地让 GC 工作与用户线程并发执行,降低停顿时间。

​ 一次CMS gc会算作两次full gc,分别为初始标记和最终标记(算上的时STW次数),但在多次收集后产生的空间碎片如果影响到了对象的分配,也会才用标记-整理算法收集一次

​ 清除算法会产生空间碎片,如果cms区预留的空闲内存不能满足新对象的分配,那么会触发Concurrent Mode Failure,这时会冻结用户线程,临时启用Serial Old收集器重新回收老年代的垃圾,全程STW,耗时很长

  • 初始标记(CMS initial mark): STW,仅标记GCRoots对象的下一个可达对象,很快
  • 并发标记(CMS concurent mark)
  • 重新标记(CMS remark): STW,解决并发标记时”那些消失的对象“
  • 并发清除(CMS concurrent sweep)

1.参数

  • -XX:+UseConcMarkSweepGC : 启用CMS收集器(年轻代默认使用ParNew收集器)

  • –XX:CMSWaitDuration=2000 : cms后台线程的轮询间隔时间(ms单位)

  • -XX:+UseCMSInitiatingOccupancyOnly : 使用基于设定的阈值进行CMS gc,值为CMSInitiatingOccupancyFraction

  • -XX:CMSInitiatingOccupancyFraction=80 : 在UseCMSInitiatingOccupancyOnly参数启用后生效。当CMS区(老年代)占比达到80%后,启用CMS垃圾回收。默认为-1,代表不启用,则老年代垃圾回收阈值算法为:**( (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0** = 92%

  • -XX:ConcGCThreads=2 :并发gc线程数,默认为(ParallelGCThreads+3)/ 4。ParallelGCThreads为新生代并行GC线程数,当CPU数量小于8时,ParallelGCThreads的值就是CPU的数量,当CPU数量大于8时,ParallelGCThreads的值等于3+5*cpuCount / 8 (可用jstack查看)

2 三色标记

2.1 含义

颜色 含义
白色 尚未被标记的对象,可能是垃圾
灰色 被标记为可达,但其内部引用的对象还没有全部扫描完
黑色 可达,且其所有引用的对象也都已经被标记(扫描完)

​ 在三色标记开始时,所有对象初始状态都是白色。GC 从 GCRoots 出发,只能扫描到 GCRoots 可达的对象。每当扫描到一个新对象时,它会先被标记为灰色(表示已经被发现但尚未处理完)。当该对象的所有引用对象也都被扫描并标记后,它就会被染为黑色(表示处理完毕,不可回收)。

而对于那些不可达的对象,由于没有任何路径从 GCRoots 可以触达它们,因此它们不会被扫描,颜色保持为白色,最终被识别为垃圾对象。

因此,在三色标记结束时,只会存在黑色和白色两类对象

  • 黑色对象:可达、已完全处理,不能被回收
  • 白色对象:不可达、未被处理,将被回收

2.2 问题

  • 浮动垃圾:被标记为黑色的对象还会继续存活。但如果我们的用户线程此时对黑色对象丢弃引用,这个黑色对象就不可达了,就应该在本次垃圾清理中被回收。但这个影响不大,下次GC可进行处理
  • 对象漏标在并发标记阶段,应用线程可能会修改对象引用关系,导致本应可达的对象未被正确扫描,仍然保持白色,最终被误回收。有如下两种情况
    • 对黑色对象A(此时A已完全扫描完毕)内部赋值一个白色对象B。B产生了漏标
    • 对灰色对象C(此时C内部还未扫描完)内部暂时断开了一个对象D使其变为白色,并在扫描完成后重新将D赋值到C中

2.3 增量更新(Incremental Update)

顾名思义,表示增加了引用。增量更新关注的是引用新增的情况,尤其是解决以下对象漏标场景:

黑色对象 A 在并发标记后,新增引用了一个未被标记的白色对象 B。

在这种情况下,为了避免漏标,写屏障机制会将 A 重新标记为灰色,使其在“重新标记(Remark)”阶段重新被扫描一次,从而发现并标记 B,确保其不会被错误回收。总结就是黑色对象A一旦新插入了白色对象B的引用之后,A就变回灰色对象了

CMS 使用增量更新策略,因为它是老年代回收器,老年代中的对象多数是长寿命的,结构稳定,引用新增比引用删除更常见。但增量更新只能处理“新增引用”,无法处理“引用删除”导致的漏标,因此并不完美。这也是 CMS 在 JDK9 被标记为过时的重要原因之一。

2.4 原始快照(Snapshot-At-The-Beginning,SATB)

保存一份并发标记开始时的引用快照,当后续并发标记过程中对这些引用删除时,都会被记录到SATB缓冲区,标记结束后SATB缓冲区的对象被重新标记为存活。

原始快照只处理对灰色对象C删除白色对象D的情况(将D记录到SATB缓冲区),重新标记阶段会在将D标为活跃。但不处理黑色新增引用,需要依赖其他机制保证(一般都是依赖写屏障,将B直接标为存活)

G1使用原始快照能完全避免对象漏标,因为它就是用写屏障直接标记白色对象为存活的方式来处理给黑色对象新增的白色对象这种漏标情况。即SATB处理删除,写屏障兜底新增。虽不可避免的会增加浮动垃圾,但肯定不会漏标

3 cms gc触发条件

  • 原文
  • foreground collector :空间分配不够触发
  • background collector
    • 显式调用 System.gc(),且配置了 -XX:+ExplicitGCInvokesConcurrent
    • 未配置 UseCMSInitiatingOccupancyOnly 时,JVM 会根据运行统计数据动态判断
    • OldGen 达到某个使用阈值(静态或动态计算)
    • Young GC 失败或预计失败,JVM 触发 CMS 作为悲观策略
    • 元空间(Metaspace)扩容触发,且 CMSClassUnloadingEnabled=true(默认开启)

4 总结

  • CMS 是一种低停顿老年代收集器,适合延迟敏感型系统。
  • 优点是并发执行、停顿低,缺点如下
    • 空间碎片严重
    • 需要预留足够空间,否则触发Concurrent Mode Failure 会退化为Serial GC,非常耗时
    • 只用了增量更新,没有完全解决漏标
    • 会产生浮动垃圾
  • 推荐配合 CMSInitiatingOccupancyFractionUseCMSInitiatingOccupancyOnly 控制触发阈值,防止内存不足时被动触发 Full GC。