CMS 收集器的工作流程是怎样的?它有哪些缺点?为什么被淘汰了?
简化版
CMS(Concurrent Mark Sweep,并发标记清除)是第一个「让 GC 线程和业务线程并发运行」以追求低停顿的老年代收集器。它分四个阶段:初始标记(STW)→ 并发标记(不停顿)→ 重新标记(STW)→ 并发清除(不停顿)——把最耗时的标记和清除放到并发阶段,只有两次短暂 STW。但它有三大硬伤:① 用「标记-清除」会产生内存碎片(大对象找不到连续空间被迫 Full GC);② 并发期间业务还在产生垃圾,形成「浮动垃圾」;③ 并发失败(Concurrent Mode Failure)会退化成单线程 STW 的 Serial Old,停顿极长。JDK 9 标记废弃、JDK 14 移除,被 G1 取代。
详细版
四个阶段:
| 阶段 | 是否 STW | 做什么 |
|---|---|---|
| ① 初始标记 Initial Mark | STW(很短) | 只标记 GC Roots 直接关联的对象 |
| ② 并发标记 Concurrent Mark | 否(并发) | 从 Roots 遍历整个对象图(三色标记,最耗时) |
| ③ 重新标记 Remark | STW(较短) | 修正并发标记期间因业务改动漏标的对象(增量更新) |
| ④ 并发清除 Concurrent Sweep | 否(并发) | 清除标记为垃圾的对象(不整理,标记-清除) |
核心思想:把最耗时的「并发标记」和「并发清除」与业务线程并行,只保留两次短 STW,从而大幅降低停顿。
三大缺点:
① 内存碎片:标记-清除不整理内存,回收后老年代满是不连续空隙
→ 大对象找不到连续空间 → 提前触发 Full GC(用 Serial Old 单线程整理,超长 STW)
② 浮动垃圾:并发清除时业务线程还在运行、还在产生新垃圾
→ 这些新垃圾本轮标记没覆盖,只能留到下次 → 所以 CMS 要预留空间给并发期间的分配
③ 并发失败 Concurrent Mode Failure:
并发回收还没做完,老年代就被业务分配满了
→ 只能停止并发,退化成 Serial Old(单线程、STW、整理整个老年代)→ 停顿几秒
为什么要预留空间:CMS 不能等老年代满了才回收(并发期间业务还要分配),必须在老年代达到某个阈值(-XX:CMSInitiatingOccupancyFraction,如 68%/92%)时就提前启动,给并发期间的分配留缓冲。
⚠️ CMS 的「并发」不是免费的——GC 线程占用 CPU,会降低业务线程的吞吐(抢 CPU)。所以 CMS 是「用吞吐换低停顿」,在 CPU 核数少时并发收集反而拖慢业务。
完整版教学
一、CMS 的目标:第一次追求「低停顿」
在 CMS 之前,老年代收集器(Serial Old、Parallel Old)都是全程 STW——标记、整理老年代时业务线程全部暂停,堆大了停顿就是几秒。对响应时间敏感的系统(Web 服务、交易系统)无法接受。
CMS 是 HotSpot 第一个并发收集器,理念是:能和业务线程一起干的活,就别停顿。标记和清除是最耗时的,CMS 把它们做成「并发」——GC 线程和业务线程同时跑,只在必须一致性的关键点做短暂 STW。这个「并发降停顿」的思路,被后来的 G1、ZGC 继承发扬。理解 CMS 是「低停顿收集器的开山鼻祖」,就理解了它的历史地位和局限。
二、四阶段详解:STW 只在头尾两次
CMS 的精髓是把工作切成四段,让长活并发、短活 STW:
时间线(业务线程 = 应用运行,GC = 回收):
初始标记[STW短] → 并发标记[并发,最长] → 重新标记[STW短] → 并发清除[并发]
↑只标 Roots直接引用 ↑遍历整个对象图 ↑修正漏标 ↑清垃圾不整理
业务暂停 业务和GC一起跑 业务暂停 业务和GC一起跑
- 初始标记(STW):只标 GC Roots 能直接引用的对象,量少,所以快。
- 并发标记(并发):从这些对象出发遍历整个对象图,这是最耗时的,但和业务并行,不停顿。
- 重新标记(STW):并发标记期间业务改了引用可能漏标,这一步用增量更新修正(重新扫描被记录的黑对象),比并发标记短但比初始标记长。
- 并发清除(并发):清除垃圾,和业务并行。
核心权衡:两次 STW(初始标记、重新标记)都很短,最耗时的两段(并发标记、并发清除)不停顿——这就是 CMS 低停顿的来源。
三、致命缺点一:内存碎片
CMS 用的是「标记-清除」算法——标记垃圾,然后直接清除(把垃圾对象占的空间标为可用),不移动存活对象、不整理内存。为什么不整理?因为整理要移动对象,移动就要更新所有引用,这在并发期间极难做到(业务还在用这些对象)。代价就是碎片:
清除前:[存活][垃圾][存活][垃圾][垃圾][存活]
清除后:[存活][ 空 ][存活][ 空 ][存活] ← 空隙不连续
问题:来了一个大对象,需要连续空间,但没有一块空隙够大
→ 明明总空闲够,却分配不下 → 被迫触发 Full GC 整理
这是 CMS 最讽刺的地方:它为了低停顿而并发清除,却因为不整理产生碎片,最终被迫触发一次超长的 STW Full GC(用 Serial Old 单线程整理整个老年代)来消除碎片——低停顿的努力被一次 Full GC 全毁了。可以用 -XX:+UseCMSCompactAtFullCollection 让 Full GC 时整理,但那次整理仍是长 STW。
四、致命缺点二:浮动垃圾与空间预留
CMS 并发运行带来「浮动垃圾」问题:
并发标记/清除进行时,业务线程还在运行:
- 还在产生新对象(可能很快变垃圾)
- 这些"并发期间新产生的垃圾",本轮标记没覆盖到
→ 只能留到下一轮 GC 才回收,这就是"浮动垃圾"
推论:CMS 不能等老年代 100% 满了才回收
因为并发期间业务还要往老年代分配对象,得留出空间
→ 用 -XX:CMSInitiatingOccupancyFraction 设一个阈值(如 68%)提前启动回收
设太高 → 并发期间空间不够 → 并发失败;设太低 → 回收太频繁
所以 CMS 必须「预留一部分空间给并发期间的分配」,这也降低了老年代的有效利用率。这是并发收集的固有代价——GC 和业务抢时间又抢空间。
五、致命缺点三:Concurrent Mode Failure
这是 CMS 最危险的失败模式。如果并发回收还没做完,老年代就被业务分配满了:
正常:并发回收在老年代满之前完成,腾出空间
失败:并发回收太慢 / 业务分配太快 / 预留空间不够
→ 老年代满了,但 CMS 还没清完 → "Concurrent Mode Failure"
→ CMS 无法继续,只能紧急退化成 Serial Old:
单线程 + 全程 STW + 整理整个老年代 → 停顿可能几秒到十几秒!
这个退化是灾难性的——本来追求低停顿,一旦并发失败,反而出现比传统收集器更长的停顿。触发原因通常是「回收阈值设太高」「老年代分配速率太快」「碎片导致提前 Full GC」。这也是 CMS 难调优的核心:要在「阈值太低频繁回收」和「阈值太高并发失败」之间走钢丝。
六、为什么被 G1 取代
CMS 的三大缺点,G1 用架构层面的改变一次性解决:
| CMS 缺点 | G1 的解法 |
|---|---|
| 标记-清除产生碎片 | Region + 复制整理,天然无碎片 |
| 停顿不可预测 | 可预测停顿模型(MaxGCPauseMillis) |
| 并发失败退化长 STW | 老年代拆成多次 Mixed GC 增量回收,压力更平缓 |
| 老年代整块处理 | 只回收「垃圾最多」的部分 Region |
所以 JDK 9 把 G1 设为默认、标记 CMS 为废弃,JDK 14 正式移除 CMS。CMS 的历史价值是「开创了并发低停顿收集」,但它的标记-清除碎片问题和并发失败是架构性缺陷,无法根治,只能被 Region 化的 G1 取代。面试问「为什么淘汰 CMS」,答案就是:碎片 + 并发失败 + 停顿不可控,这些是标记-清除 + 连续分代结构的固有缺陷,G1 用 Region + 复制整理 + 可控停顿从根上解决了。
记忆钩子:「CMS 四阶段:初始标记(STW)→并发标记→重新标记(STW)→并发清除;三大坑:标记清除生碎片(逼出 Full GC)、并发产生浮动垃圾(要预留空间)、并发失败退化 Serial Old(长 STW);被 G1 取代」。
七、常见误区与追问
- 误区:CMS 全程不停顿。 有两次 STW(初始标记、重新标记),只是把最耗时的并发标记和并发清除与业务并行;重新标记的 STW 有时还不短。
- 误区:CMS 用标记-整理算法。 用的是标记-清除(不移动对象),所以产生碎片;这正是它被迫触发 Full GC 的原因。
- 误区:CMS 是年轻代收集器。 它是老年代收集器,通常搭配 ParNew(年轻代)使用;年轻代仍是复制算法。
- 误区:CMS 停顿一定比 Parallel 短。 正常时短,但一旦 Concurrent Mode Failure 退化成 Serial Old,停顿反而比 Parallel Old 长得多。
- 追问:什么是 Concurrent Mode Failure? 并发回收还没完成、老年代就被分配满了,CMS 无法继续,退化成单线程 STW 的 Serial Old 整理整个老年代,产生超长停顿。
- 追问:CMS 为什么要预留空间/提前触发回收? 并发回收期间业务线程还在往老年代分配对象,若等满了才回收会并发失败,所以用 CMSInitiatingOccupancyFraction 在阈值(如 68%)就提前启动。
- 追问:CMS 用哪种写屏障解决并发标记漏标? 增量更新(Incremental Update)——记录「黑对象新增指向白对象的引用」,在重新标记阶段重新扫描这些黑对象;这和 G1 的 SATB 不同。
八、加强记忆
CMS 是 HotSpot 第一个追求低停顿的并发老年代收集器,理念是「能和业务并行的活就别停顿」。四阶段:初始标记(STW,只标 Roots 直接引用)→ 并发标记(与业务并行,遍历对象图,最耗时)→ 重新标记(STW,用增量更新修正漏标)→ 并发清除(与业务并行)——把最耗时的两段并发化,只留两次短 STW。但有三大架构性缺陷:① 标记-清除不整理内存产生碎片(大对象找不到连续空间,被迫触发单线程 STW 的 Full GC,低停顿努力全毁);② 并发期间业务仍产生浮动垃圾(要预留空间、提前按阈值触发回收);③ Concurrent Mode Failure(并发没做完老年代就满了,退化成 Serial Old 单线程 STW 整理,停顿几秒到十几秒,灾难性)。这些是「标记-清除 + 连续分代」的固有问题,无法根治,被 G1 用「Region + 复制整理 + 可控停顿」取代,JDK 9 废弃、14 移除。一句话「四阶段两次 STW、标记清除生碎片、浮动垃圾要预留、并发失败退化长 STW,注定被 G1 取代」。