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+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
底层原理掌握度:面试官不仅仅是想知道 CMS 是什么,更是想看你能不能把它的四个阶段说清楚,哪些阶段 STW、哪些阶段并发,为什么这么设计。
-
优劣势分析能力:CMS 不是一个完美的收集器,它有三个著名的硬伤——内存碎片、浮动垃圾、并发模式失败。能不能把这些说清楚,直接体现你的深度。
-
演进认知: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 的三大优势
-
低延迟:这是 CMS 最大的卖点。两个最耗时的阶段(并发标记、并发清除)都不暂停用户线程,STW 时间被压到了毫秒级。对于延迟敏感的应用(比如交易系统、实时报价),这是质的飞跃。
-
并发能力强:GC 线程和用户线程并发执行,CPU 利用率更高。当然也不是没有代价——并发阶段 GC 线程会占用一部分 CPU 资源,默认启动
(CPU 核心数 + 3) / 4个 GC 线程。 -
适合老年代收集: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 时间。这个在生产环境几乎是标配。
面试高频追问
-
CMS 和 G1 有什么区别?
CMS 是老年代收集器,用 "标记-清除",有碎片问题。G1 把堆分成 Region,用 "标记-整理",可以控制停顿时间目标(
-XX:MaxGCPauseMillis),不需要手动调那么多参数。G1 在大堆(6GB+)场景下表现更好。 -
CMS 为什么被废弃了?
维护成本太高。CMS 的代码又老又复杂,和很多其他组件有耦合,每次优化 JVM 都要考虑 CMS 的兼容性。而且 G1 在 JDK 9 已经足够成熟,能覆盖 CMS 的绝大多数场景。
-
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 留下的这些问题。
