CMS 垃圾收集器:并发标记 - 清除的兴衰与 JVM 低延迟探索启示

121 阅读6分钟

CMS垃圾收集器:原理、实践与辩证思考

CMS(Concurrent Mark Sweep,并发标记-清除)是JVM中一款以“低延迟”为核心目标的垃圾收集器,专为老年代回收设计。它诞生于Java 5(2004年),在G1成熟前长期作为“低延迟场景首选”,但因设计局限性,在Java 9被标记为 deprecated(过时),Java 14正式移除。尽管已退出主流,但其设计思想对后续GC(如G1、ZGC)影响深远,理解CMS仍有重要价值。

一、CMS的核心原理:“并发”如何实现低延迟?

CMS的核心逻辑是将耗时的“标记”和“清除”阶段与应用线程并发执行,仅保留两个短暂的停顿阶段,从而最大限度减少GC对业务的影响。其完整流程分为5个阶段:

1. 初始标记(Initial Mark)

  • 目标:标记“根对象”直接引用的老年代对象(如年轻代存活对象引用的老年代对象、静态变量引用的对象)。
  • 特点:需要停顿应用线程(STW,Stop-The-World),但耗时极短(通常<10ms),因为仅标记根直接引用的对象,不遍历整个引用链。

2. 并发标记(Concurrent Mark)

  • 目标:从初始标记的对象出发,遍历并标记所有存活的老年代对象。
  • 特点:与应用线程并发执行(无STW),耗时较长(取决于老年代大小和对象存活比例),但不阻塞业务。
  • 隐患:并发标记期间,应用线程可能修改对象引用(如删除旧引用、创建新引用),导致部分对象漏标或误标。

3. 并发预清理(Concurrent Preclean)

  • 目标:处理并发标记期间因应用线程操作产生的“引用变更”(如通过写屏障记录的“脏对象”),减少后续“重新标记”的工作量。
  • 特点:仍与应用线程并发,进一步优化标记效率。

4. 重新标记(Remark)

  • 目标:修正并发标记期间因应用线程干扰导致的标记错误,确保所有存活对象被正确标记。
  • 特点:需要STW,但通过“三色标记+增量更新”算法(记录并发期间的引用变更),停顿时间通常比初始标记长(但仍远短于Full GC)。

5. 并发清除(Concurrent Sweep)

  • 目标:清除所有未被标记的垃圾对象(已死亡的老年代对象),释放内存空间。
  • 特点:与应用线程并发执行(无STW),仅回收空间不移动对象(因此会产生内存碎片)。

核心设计亮点:通过“并发标记”和“并发清除”将GC的大部分工作与业务线程重叠,仅用两个短停顿完成关键操作,实现“低延迟”目标。

二、CMS的使用方法:参数与配置

启用CMS需JDK 5及以上版本(但建议仅在维护旧系统时使用),核心JVM参数如下:

# 启用CMS垃圾收集器(老年代用CMS,年轻代默认用ParNew)
-XX:+UseConcMarkSweepGC

# 年轻代使用ParNew收集器(与CMS配合最佳,默认启用)
-XX:+UseParNewGC

# 设置老年代占用率阈值,达到该值时触发CMS(默认约92%)
# 例如:老年代使用60%时触发,避免内存不足导致Full GC
-XX:CMSInitiatingOccupancyFraction=60
-XX:+UseCMSInitiatingOccupancyOnly  # 强制使用上面的阈值,不动态调整

# 解决内存碎片问题:Full GC时对老年代进行压缩(默认开启)
-XX:+UseCMSCompactAtFullCollection
# 每多少次CMS后触发一次压缩(默认0,即每次Full GC都压缩)
-XX:CMSFullGCsBeforeCompaction=3

# 允许CMS回收永久代/元空间(JDK 8前适用)
-XX:+CMSClassUnloadingEnabled

三、CMS的优势:为何曾是低延迟首选?

  1. 低延迟特性突出
    核心阶段(并发标记、并发清除)与应用线程并行,STW停顿主要集中在“初始标记”和“重新标记”,单次停顿通常可控制在10-100ms,远优于传统SerialGC(秒级)和ParallelGC(百毫秒级),适合对响应时间敏感的场景(如电商支付、实时交易)。

  2. 吞吐量影响较小
    并发执行机制减少了GC对业务线程的阻塞时间,相比“全程STW”的GC,对系统吞吐量的影响更小(尤其在大堆场景下)。

  3. 成熟稳定
    经过十余年迭代优化,在JDK 7/8中表现稳定,适配多数企业级应用(如Web服务、中间件),调优经验丰富。

四、CMS的劣势:被淘汰的根源

  1. 内存碎片严重
    并发清除阶段仅删除垃圾对象,不移动存活对象,导致老年代长期运行后产生大量内存碎片。当需要分配大对象时,可能因找不到连续空间触发Full GC(单线程压缩,STW时间极长)。

  2. CPU资源消耗高
    并发阶段(标记、清除)需要额外的GC线程与应用线程竞争CPU资源(通常需要2-4个GC线程),在CPU核心较少的机器上(如2核4G),可能导致业务线程响应变慢(吞吐量下降5%-20%)。

  3. 浮动垃圾(Floating Garbage)问题
    并发清除阶段,应用线程可能新产生垃圾(未被标记),这些“浮动垃圾”需等到下一次GC才能回收,因此需要预留部分老年代空间(通常10%-20%),否则可能因内存不足触发Full GC。

  4. 对大堆支持有限
    老年代超过10GB后,并发标记和清除的耗时显著增加,可能导致GC周期赶不上对象分配速度,最终触发Full GC。

  5. 已被官方废弃
    随着G1(兼顾延迟与吞吐量)、ZGC(亚毫秒级延迟)的成熟,CMS的设计缺陷(碎片、CPU开销)难以修复,官方在Java 9后不再维护,仅作为兼容选项存在。

五、适用场景与替代方案

  • 适合场景:仅推荐在维护旧系统时使用(如基于JDK 8的遗留服务),且满足:堆内存中等(2-10GB)、对延迟敏感(允许100ms内停顿)、CPU资源充足(4核以上)。
  • 替代方案
    • 中等堆(4-32GB)+ 平衡延迟与吞吐量 → 选择G1;
    • 大堆(32GB以上)+ 亚毫秒级延迟 → 选择ZGC(Java 11+)或Shenandoah(OpenJDK);
    • 小堆(<4GB)+ 吞吐量优先 → 选择ParallelGC。

六、深度思考:CMS的历史价值与启示

CMS的兴衰揭示了JVM GC的发展逻辑:从“单一目标优化”到“综合场景适配”。它首次将“并发”思想大规模应用于GC,证明了“低延迟”的可行性,为后续G1的“Region化+混合回收”、ZGC的“着色指针+读屏障”提供了实践基础。

同时,CMS的缺陷也警示我们:没有“万能GC”,只有“适合场景的GC”。选择GC时,需结合堆大小、延迟需求、CPU资源、JDK版本综合评估,而非盲目追求“新技术”或固守“老经验”。

总结

CMS是JVM GC发展史上的重要里程碑,以“并发标记-清除”实现了低延迟突破,但因内存碎片、CPU开销等固有缺陷被时代淘汰。理解其原理与局限,不仅能更好地维护旧系统,更能深刻把握JVM垃圾回收的演进逻辑——平衡“延迟、吞吐量、内存开销”的永恒追求