06-JVM垃圾回收器之CMS

本篇是「JVM 与性能调优系列」第 6 篇。

G1 之前,CMS 是低延迟的代名词。它敢在程序运行的同时回收老年代。但它也留下了「碎片化的诅咒」,最终被官方废弃。看懂 CMS,才懂 G1 为什么是答案。


一、CMS 要解决什么

Parallel Old 回收老年代时是全程 STW ,堆一大,停顿动辄几百毫秒------对 Web 服务是灾难。CMS(Concurrent Mark Sweep)的目标很明确:降低老年代回收的 STW,让大部分回收工作和业务线程并发进行

核心思想:把「能并发的尽量并发」,只保留极短的 STW 段。代价是算法更复杂、有碎片副作用。


二、CMS 的四个阶段

CMS 的老年代回收分四步,其中两步极短 STW,两步完全并发:

  1. 初始标记(STW,极短) :只标记 GC Roots 直接关联的对象(数量少,快)
  2. 并发标记(并发):从初始标记的对象出发,沿引用链遍历整个老年代。这段时间业务线程照常跑
  3. 重新标记(STW,较短):并发标记期间,业务线程可能改了引用关系,导致标记有偏差。这一步把变动修正过来(用「增量更新」)
  4. 并发清除(并发):清除死亡对象,用标记-清除算法,不挪动存活对象

注意第 2、4 步是并发的,所以业务几乎不感知------这正是 CMS 低延迟的来源。但「重新标记」阶段仍要 STW,它扫的是并发期间变动的引用,通常比初始标记略长。


三、三色标记与重新标记

并发标记用三色标记追踪存活:

  • 白:尚未访问(可能是垃圾)
  • 灰:已访问自身,但引用还没扫完
  • 黑:自身和引用都扫完,确定存活

问题来了:并发标记时,业务线程可能「把一个白对象挂到黑对象下,同时断开灰对象到它的引用」------白对象会被误判为垃圾(漏标)。

CMS 用**增量更新(Incremental Update)**补救:只要黑对象新插入了白对象的引用,就把它记下来,重新标记时重新扫。G1 换了个思路用 SATB(下一篇讲)。两种都是「并发标记如何不漏标」的解法。


四、CMS 的两大原罪

CMS 证明了并发回收的价值,却栽在两个硬伤上:

原罪 1:碎片。 CMS 用标记-清除,不清空后不整理,老年代会留下越来越多空洞。某天要分配一个大对象,总空闲够却找不到连续空间 → 晋升失败 → 退化成 Serial Old 做一次 Full GC(巨慢)

原罪 2:并发失败。 并发清除期间,业务还在不停 new 对象,老年代被填充。若填充速度超过清除速度、预留空间(默认 92% 触发)被击穿 → Concurrent Mode Failure → 同样退化成 Serial Old。


五、关键参数

参数 作用
-XX:+UseConcMarkSweepGC 启用 CMS
-XX:CMSInitiatingOccupancyFraction 老年代占用到多少触发 CMS(默认 92%)
-XX:+UseCMSCompactAtFullCollection Full GC 时做碎片整理
-XX:CMSFullGCsBeforeCompaction 每隔几次 Full GC 整理一次
-XX:+CMSParallelRemarkEnabled 重新标记阶段并行,缩短 STW

调优要点:如果频繁 Concurrent Mode Failure,把 CMSInitiatingOccupancyFraction 调低(比如 70%),让 CMS 更早启动;但太低会 GC 太频繁。


六、为什么被废弃

  • JDK 9 标记为废弃 ,JDK 14 彻底移除
  • 维护成本高、碎片化无解、无法与新特性(如 ZGC 的染色指针)共存
  • 官方明确推荐迁移到 G1(JDK 9 起默认)

今天新项目基本不该用 CMS 了,但理解它仍重要------很多老系统还在跑,而且 G1 的设计正是对 CMS 缺陷的回应。


七、承上启下

CMS 用「并发标记 + 标记清除」把老年代停顿降下来,却败给碎片。G1 的解决方案是:把堆切成小 Region,每次只回收价值最高的 Region,并在回收时做整理------既并发,又无碎片。


八、实战:一次 Concurrent Mode Failure 复盘

某电商服务,老年代 4G,CMSInitiatingOccupancyFraction=92。大促时突发流量,对象以 300MB/s 的速度晋升老年代。CMS 在 92%(约 3.7G)才启动并发标记,但标记+清除速度跟不上业务填充速度,老年代很快冲破上限 → 触发 Concurrent Mode Failure → 退化 Serial Old 全堆整理 → 一次 2.3 秒的全局停顿,接口全部超时。

根因不是 CMS 不行,是触发阈值太高。把 CMSInitiatingOccupancyFraction 调到 70%,让 CMS 提前启动,并发阶段有充足时间跟上业务速度,问题消失。


九、CMS 与日志的对应

开启 -XX:+PrintGCDetails 后,CMS 的周期在日志里长这样:

复制代码
CMS-initial-mark → CMS-concurrent-mark → CMS-concurrent-preclean
→ CMS-remark (STW) → CMS-concurrent-sweep → CMS-concurrent-reset

看到 concurrent mode failure 字样,就对应上面的原罪 2,优先调低触发阈值。


十、CMS 废弃后的迁移路径

如果你的老系统还在用 CMS(-XX:+UseConcMarkSweepGC),JDK 14+ 会直接启动失败。迁移建议:

  1. 首选 G1:JDK 9+ 默认,几乎无需改业务代码,停顿通常比 CMS 更稳、无碎片。多数 CMS 应用平迁 G1 即可
  2. 低延迟需求迁 ZGC:若对停顿极敏感且能升 JDK 17/21,ZGC 是终极答案
  3. 保留 Parallel 的场景:纯吞吐批处理,且不想动 JDK,Parallel 也完全够用

迁移不是换个参数那么简单:要先在测试环境压测,对比停顿与吞吐,确认新收集器在你的真实负载下达标,再上生产。别信「换个收集器自动变快」的神话。


十一、CMS 并发模式失败深度剖析

Concurrent Mode Failure 是 CMS 最典型的崩溃,值得深挖它的触发链:

  1. CMS 在老年代占用到 CMSInitiatingOccupancyFraction(默认 92%)时才启动并发周期
  2. 并发标记 + 并发清除期间,业务线程仍在疯狂分配,老年代持续被填充
  3. 如果填充速度 > 清除速度,老年代在周期结束前就见了底
  4. CMS 无力回天 → 冻结所有线程 → 退化成 Serial Old 做一次全堆、单线程、标记-整理的 Full GC

这次退化 Full GC 可能耗时 数秒 (堆越大越久),期间所有请求超时。根因不是 CMS 慢,是触发太晚 + 分配太快 。把 CMSInitiatingOccupancyFraction 调到 70%~80%,让并发周期早启动、留足缓冲,就能避免。


总结

CMS 是低延迟的先驱:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除,把大部分工作并发化。但它用标记-清除留下碎片 ,并发期间又可能并发失败,两者都逼它退化成 Serial Old。它的退役,恰好引出了下一篇的主角 G1。

相关推荐
黑马程序员毕设1 小时前
基于Java的仪器管理系统设计与实现
java·开发语言·spring boot·后端·微信小程序
骇客野人1 小时前
SpringBoot电商购物车、结算、下单、库存方案设计与落地实施步骤
java·spring boot·后端
哪 吒2 小时前
华为OD机试 - 云服务安全策略最优选择 - 深度优先搜索DFS(Java 新系统 200分)
java·华为od·深度优先
拒绝内耗。2 小时前
程序员学架构(一):一张图看懂 Java 后端架构:一个请求怎样从手机到数据库?
java·智能手机·架构
棣廷2 小时前
初识Python面向对象
java·开发语言
用户3126874877202 小时前
ThreadLocal 到底会不会内存泄漏?源码级拆解 + 线程池陷阱
java
珍珠先生2 小时前
P11 · MySQL 驱动版本过旧:Client does not support authentication protocol
java
~木雨2 小时前
Java 并发编程架构全景:并发层的五域设计 —— 线程模型、线程池隔离、并发安全到问题治理(全体系汇总)
java·安全·架构·线程池·并发编程·高并发架构
极小狐2 小时前
从复制粘贴到可复用组件:极狐GitLab CI/CD 组件目录实战
java·ci/cd·gitlab·devops·组件化