CMS 优缺点与碎片问题

候选人小赵在面试蚂蚁 P7 时,面试官问道:

"CMS 和 G1 相比,有什么缺点?"

小赵说:"CMS 会产生内存碎片..."面试官追问:"碎片问题具体会导致什么问题?"

小赵答不上来。面试官继续追问:"为什么 JDK 14 把 CMS 废弃了?"

小赵彻底卡住了...

一、核心问题:CMS 优缺点 🔴

1.1 问题拆解

第一层:优点(有什么好?)
  "CMS 的核心优势是什么?"
  考察点:并发标记、停顿时间

第二层:缺点(有什么不好?)
  "CMS 有哪些问题?"
  考察点:内存碎片、浮动垃圾、Concurrent Mode Failure

第三层:替代方案(怎么替代?)
  "G1 是怎么解决 CMS 的碎片问题的?"
  考察点:Region 划分、Remembered Set

1.2 ❌ 错误示范

候选人原话 A:"CMS 比 G1 更快,所以应该用 CMS。"

问题诊断:CMS 的并发阶段消耗 CPU,降低应用吞吐量。在高并发场景下,CMS 的吞吐量通常低于 G1。

候选人原话 B:"G1 完美解决了 CMS 的所有问题。"

问题诊断:G1 解决了碎片问题,但引入了新的问题——Remembered Set 的内存开销、YGC 停顿等。

1.3 标准回答

P5 级别:CMS 的优势

CMS 的核心优势:并发标记

CMS 的最大特点是并发标记并发清除,在大部分阶段与用户线程并发运行,减少了 Stop-The-World 的时间:

阶段Serial GCCMS
初始标记STWSTW(短)
并发标记并发(长)
重新标记STW(较短)
并发清除并发(长)

适用场景

  • 对停顿时间有要求的交互式应用
  • 老年代对象相对稳定的场景

P6 级别:CMS 的三大问题

问题 1:内存碎片

CMS 使用标记-清除算法,产生大量内存碎片:

graph TD
    A[碎片化内存] -->|分配大对象| B[无法找到连续空间]
    B --> C[触发 Full GC(Serial Old)<br/>停顿时间数秒]

碎片化的后果

  • 大对象分配失败
  • 触发 Serial Old 的 Full GC(最长的停顿)

问题 2:浮动垃圾

并发标记期间产生的"浮动垃圾"需要等待下一次 GC 才能回收:

并发标记开始 → 对象 A 被标记为可达
→ 对象 A 在并发清除前变成不可达(用户线程修改)
→ 对象 A 成为浮动垃圾
→ 下一次 GC 才回收

问题 3:Concurrent Mode Failure

如果在并发清除期间老年代满了,无法容纳新分配对象 + 浮动垃圾:

# 日志
[Full GC [CMS: concurrent-mode-failure] # CMS 并发模式失败
[Full GC [CMS: allocation-failure]      # 分配失败

触发 Serial Old Full GC,停顿时间可能达到 10 秒以上。

P7 级别:G1 的解决思路

G1 如何解决碎片问题

G1 使用标记-整理算法(而非标记-清除):

graph TD
    G1[G1 Region 划分] --> G1_1[Eden Region]
    G1 --> G1_2[Survivor Region]
    G1 --> G1_3[Old Region]
    G1 --> G1_4[Humongous Region]

    G1_3 -->|回收时| G1_5[标记-整理<br/>所有存活对象向一端移动]

G1 的整理是并发的,不会产生长时间的 STW。

G1 的停顿时间控制

G1 支持设置停顿时间目标:

-XX:MaxGCPauseMillis=200  # 期望最大停顿时间 200ms

G1 通过Mixed GC(混合 GC)来满足停顿时间目标——在一次停顿中只回收部分 Region。

【面试官心理】 这道题我能问到 P7 级别,是因为 CMS vs G1 的对比涉及了 GC 演进的工程权衡。能说清碎片问题导致 Serial Old Full GC 的候选人说明他有过生产调优经验。

1.4 追问升级

追问:JDK 14 废弃 CMS 时,推荐用什么替代?

  • JDK 8 及之前:ParNew + CMS → G1
  • JDK 11+:G1(默认)、ZGC(超低停顿)

二、生产调优 🟡

2.1 CMS 参数调优

# 减少碎片化
-XX:+UseCMSCompactAtFullCollection  # Full GC 时整理内存
-XX:CMSFullGCsBeforeCompaction=5   # 5 次 Full GC 后整理

# 提前启动 CMS
-XX:CMSInitiatingOccupancyFraction=70  # 老年代 70% 开始回收
-XX:+UseCMSInitiatingOccupancyOnly     # 只使用上述阈值

2.2 G1 的配置

# G1 推荐配置
-XX:MaxGCPauseMillis=200   # 停顿时间目标
-XX:G1HeapRegionSize=8m     # Region 大小(1/2/4/8/16/32 MB)
-XX:InitiatingHeapOccupancyPercent=45  # 堆使用率 45% 时开始并发周期
💡

面试加分点:能说出"JDK 11 的 ZGC 通过着色指针和读屏障,实现了停顿时间 < 1ms 的目标,且停顿时间不随堆大小增加而增加(停顿时间和存活对象数量无关,只和 Roots 数量有关)",说明他关注了 ZGC 的技术突破。

⚠️

面试陷阱:被问到"CMS 产生碎片后,能通过配置解决吗",很多人会说"可以"。准确答案是:只能缓解,不能根本解决-XX:CMSFullGCsBeforeCompaction 可以定期整理,但整理本身需要 Full GC。根本解决方案是迁移到 G1。