← 返回题目列表

CMS 收集器的工作流程是怎样的?它有哪些缺点?为什么被淘汰了?

困难 第 30 / 34 题 更新于 2026/07/26
CMS并发收集浮动垃圾Concurrent Mode Failure

简化版

CMS(Concurrent Mark Sweep,并发标记清除)是第一个「让 GC 线程和业务线程并发运行」以追求低停顿的老年代收集器。它分四个阶段:初始标记(STW)→ 并发标记(不停顿)→ 重新标记(STW)→ 并发清除(不停顿)——把最耗时的标记和清除放到并发阶段,只有两次短暂 STW。但它有三大硬伤:① 用「标记-清除」会产生内存碎片(大对象找不到连续空间被迫 Full GC);② 并发期间业务还在产生垃圾,形成「浮动垃圾」③ 并发失败(Concurrent Mode Failure)会退化成单线程 STW 的 Serial Old,停顿极长。JDK 9 标记废弃、JDK 14 移除,被 G1 取代。

详细版

四个阶段

阶段是否 STW做什么
① 初始标记 Initial MarkSTW(很短)只标记 GC Roots 直接关联的对象
② 并发标记 Concurrent Mark否(并发)从 Roots 遍历整个对象图(三色标记,最耗时)
③ 重新标记 RemarkSTW(较短)修正并发标记期间因业务改动漏标的对象(增量更新)
④ 并发清除 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 取代」。