CMS 的底层原理是什么?优势在哪?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 底层原理掌握度:面试官不仅仅是想知道 CMS 是什么,更是想看你能不能把它的四个阶段说清楚,哪些阶段 STW、哪些阶段并发,为什么这么设计。

  2. 优劣势分析能力:CMS 不是一个完美的收集器,它有三个著名的硬伤——内存碎片、浮动垃圾、并发模式失败。能不能把这些说清楚,直接体现你的深度。

  3. 演进认知:CMS 在 JDK 9 被标记为废弃,JDK 14 正式移除。面试官想知道你是否了解收集器的演进路线,以及为什么 G1 能替代它。

核心答案

CMS 的核心目标就一个:最小化老年代 GC 的停顿时间。它通过让大部分 GC 工作与用户线程并发执行来实现这一点。

CMS 采用 "标记-清除" 算法,整个回收过程分为 4 个阶段

阶段 是否 STW 做什么
初始标记(Initial Mark) ✅ STW 标记 GC Roots 直接引用的对象,速度很快
并发标记(Concurrent Mark) ❌ 并发 从 GC Roots 遍历整个对象图,耗时最长
重新标记(Remark) ✅ STW 修正并发标记期间变动的引用关系
并发清除(Concurrent Sweep) ❌ 并发 清除死亡对象,释放内存

两个 STW 阶段都很短,真正的耗时大户(并发标记和并发清除)都不暂停用户线程,这就是 CMS 低延迟的秘诀。

深度解析

一、四个阶段详解

整体分四个阶段,一个一个说:

  • 初始标记:这个阶段就是扫描一下 GC Roots 能直接够到的对象。因为只看 "第一层",所以速度非常快,通常只有几毫秒的 STW。但注意,它依赖 Minor GC 的结果来获得老年代的 GC Roots。

  • 并发标记:这是最耗时的阶段。从初始标记得到的对象出发,沿着引用链一路遍历下去,把所有存活的对象都标记出来。关键是——这个过程用户线程照常跑。这也就意味着,用户线程在跑的同时可能产生新的引用变动,CMS 用的是 "增量更新"(Incremental Update) 来记录这些变化。

  • 重新标记:这个阶段是 STW 的,用来修正并发标记期间发生的引用变化。为了缩短重新标记的停顿时间,CMS 引入了 可中断的预清理(CMS-concurrent-preclean) 阶段,在并发标记结束后先做一些预处理,这样重新标记阶段的负担就小了。即便如此,重新标记仍然是 CMS 中最长的 STW 阶段。

  • 并发清除:用 "标记-清除" 算法把死亡对象干掉,释放内存。这个阶段也是并发的,用户线程正常执行。但正因为用户线程还在跑,所以在清理的过程中,用户线程还在产生新对象——这就要求 CMS 必须预留足够的内存空间给用户线程使用。

二、CMS 的三大优势

  1. 低延迟:这是 CMS 最大的卖点。两个最耗时的阶段(并发标记、并发清除)都不暂停用户线程,STW 时间被压到了毫秒级。对于延迟敏感的应用(比如交易系统、实时报价),这是质的飞跃。

  2. 并发能力强:GC 线程和用户线程并发执行,CPU 利用率更高。当然也不是没有代价——并发阶段 GC 线程会占用一部分 CPU 资源,默认启动 (CPU 核心数 + 3) / 4 个 GC 线程。

  3. 适合老年代收集:CMS 专门针对老年代设计,通常和 ParNew 搭配使用(ParNew + CMS),形成一套完整的新生代 + 老年代收集方案。

三、CMS 的三个硬伤

这块面试官特别爱问,因为能体现你对 CMS 的理解是不是停留在表面。

问题 原因 影响
内存碎片 "标记-清除"算法不压缩内存 长时间运行后,大对象分配失败触发 Full GC
浮动垃圾 并发清除阶段用户线程产生的新垃圾 只能等下一次 GC 回收,内存利用率降低
并发模式失败 老年代预留空间不够 退化为 Serial Old 收集器,STW 时间暴涨

一个一个展开说:

内存碎片:因为 CMS 用的是 "标记-清除" 而非 "标记-整理",清理完之后内存空间是不连续的。碎片积累到一定程度,虽然总剩余空间足够,但找不到一块连续的大空间来分配大对象,这时候就不得不触发一次 Full GC 来做内存整理。

解决办法是 -XX:+UseCMSCompactAtFullCollection(默认开启),在 Full GC 后做一次内存压缩。但这又引入了 STW... 所以 CMS 有个参数 -XX:CMSFullGCsBeforeCompaction,允许你设置执行多少次不带压缩的 Full GC 后才做一次压缩,默认值是 0,也就是每次都压缩。

浮动垃圾:并发清除阶段用户线程还在跑,自然会产生新的对象引用变动。有些对象在并发标记阶段还是存活的,到了清除阶段其实已经变成了垃圾,但 CMS 已经来不及处理了。这些就是 "浮动垃圾",只能等下一轮 GC 来收拾。

并发模式失败(Concurrent Mode Failure):这个最要命。CMS 在并发清理时用户线程还在分配内存,所以不能等老年代满了才开始 GC,必须预留一部分空间。参数 -XX:CMSInitiatingOccupancyFraction 控制触发阈值,默认是 92%(JDK 6 之后会自动调整)。如果预留空间不够用(比如突然来了一波大流量),老年代空间耗尽,CMS 只能退化为 Serial Old 收集器——用单线程做一次完整的 "标记-整理",停顿时间可能飙到几秒甚至十几秒。这基本等于 CMS 辛辛苦苦压下来的延迟白费了。

四、CMS 相关核心参数

参数 含义 推荐值
-XX:+UseConcMarkSweepGC 启用 CMS
-XX:CMSInitiatingOccupancyFraction 老年代使用率达到多少触发 CMS 70-80
-XX:+UseCMSCompactAtFullCollection Full GC 后做内存压缩 默认开启
-XX:CMSFullGCsBeforeCompaction 多少次 Full GC 后做压缩 0(每次都压)
-XX:ConcGCThreads 并发 GC 线程数 默认 (CPU+3)/4
-XX:+CMSParallelRemarkEnabled 并行重新标记 建议开启
-XX:+CMSScavengeBeforeRemark 重新标记前先做一次 Minor GC 建议开启

最后两个参数要特别说一下。CMSScavengeBeforeRemark 在重新标记前先做一次 Minor GC,这样可以减少新生代对象对重新标记的干扰,显著缩短重新标记的 STW 时间。这个在生产环境几乎是标配。

面试高频追问

  1. CMS 和 G1 有什么区别?

    CMS 是老年代收集器,用 "标记-清除",有碎片问题。G1 把堆分成 Region,用 "标记-整理",可以控制停顿时间目标(-XX:MaxGCPauseMillis),不需要手动调那么多参数。G1 在大堆(6GB+)场景下表现更好。

  2. CMS 为什么被废弃了?

    维护成本太高。CMS 的代码又老又复杂,和很多其他组件有耦合,每次优化 JVM 都要考虑 CMS 的兼容性。而且 G1 在 JDK 9 已经足够成熟,能覆盖 CMS 的绝大多数场景。

  3. CMS 的 "增量更新" 和 G1 的 "原始快照" 有什么区别?

    增量更新(CMS):记录的是新增的引用。重新标记阶段只需要重新扫描这些新增引用指向的对象。 原始快照(G1/SATB):记录的是被删除的引用。确保这些引用指向的对象不会被遗漏。 两种方案都是为了在并发标记阶段保证不漏标存活对象,只是思路不同。

常见面试变体

  • "CMS 回收器有几个阶段?哪些会 STW?"
  • "CMS 有什么缺点?生产环境怎么解决这些问题?"
  • "CMS 什么情况下会退化为 Serial Old?"
  • "为什么 JDK 14 要移除 CMS?"

记忆口诀

四阶段:初标快(STW)→ 并发标(最长)→ 重新标(STW)→ 并发清

三硬伤:碎片、浮动垃圾、并发模式失败

一句话理解 CMS:用 "并发" 换 "低延迟",代价是内存碎片和浮动垃圾,最怕的是空间不够退化成 Serial Old。

总结

CMS 的核心就是 "标记-清除" + 并发,通过把最耗时的标记和清除阶段做成并发的,实现了毫秒级的 STW 停顿。但它不是完美的——内存碎片、浮动垃圾、并发模式失败这三个问题始终伴随着它。理解了 CMS 的设计取舍,再看 G1、ZGC 的演进路线,就会发现它们本质上都是在解决 CMS 留下的这些问题。