CMS 的底层原理是什么?优势在哪?
结论先行:CMS(Concurrent Mark Sweep)是第一款真正意义上的并发老年代收集器,把「标记」和「清除」都拆成可与用户线程并行的阶段,只有初始标记和重新标记两次短暂 STW。它用**增量更新(incremental update)**配合写后屏障解决并发标记的漏标问题,代价是不压缩内存、产生浮动垃圾,最终被 G1 取代。
一、四个阶段
| 阶段 | 是否 STW | 做什么 | 耗时 |
|---|---|---|---|
| 初始标记 | 是 | 标记 GC Roots 直接可达的对象 | 极短,与根数量相关 |
| 并发标记 | 否 | 从根出发遍历对象图,同时处理写屏障记录 | 最长,与应用并行 |
| 重新标记 | 是 | 处理增量更新队列,修正漏标对象 | 短,但受脏卡数量影响 |
| 并发清除 | 否 | 清除不可达对象,不移动存活对象 | 较长,与应用并行 |
时间轴(一个 CMS 周期):
应用线程 ──████████████████████████████████████──
CMS 线程 ██ ███████████████ ██████████
初始标记 并发标记 重新标记 并发清除
(STW) (STW)
二、增量更新如何防漏标
并发标记时应用线程在改引用,可能出现「黑色对象新增了指向白色对象的引用」而标记线程已扫过该黑对象,导致白对象被误清。CMS 的写后屏障在引用赋值完成后把被赋值的引用记录到队列:
// 概念示意:CMS 的写后屏障(实际由 JIT 在字节码层面插入)
void oop_field_store(oop* field, oop new_value) {
*field = new_value; // 先真正赋值
if (is_marking() && new_value != null && !is_black(new_value)) {
enqueue_for_revisit(field); // 再把新增引用入队,重新标记阶段重扫
}
}
重新标记阶段会以 GC Roots 与这些队列为起点重扫一遍,因此 CMS 的 remark 停顿与「并发期间修改的引用数量」成正比。这也是 CMSScavengeBeforeRemark 存在的原因:先做一次 Young GC,减少新生代对象晋升带来的脏卡扫描量。
三、优势与代价
| 优势 | 代价 |
|---|---|
| 停顿短,适合响应敏感的服务 | 不压缩,产生碎片,大对象分配易失败 |
| 并发清除不移动对象,无需更新引用 | 浮动垃圾只能下轮回收,需预留空间 |
| 实现成熟,JDK 8 上大量生产验证 | 并发模式失败后退化为单线程 Full GC,停顿从毫秒变秒级 |
| 可与 ParNew 组合覆盖新生代 | 并发线程抢占 CPU,吞吐下降约 10%~20% |
四、碎片与失败怎么治理
# 提前启动并发收集,给浮动垃圾留余量
java -XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSInitiatingOccupancyOnly \
-XX:+CMSParallelRemarkEnabled \
-XX:+CMSScavengeBeforeRemark \
-XX:CMSScheduleRemarkEdenPenetration=50 \
-jar app.jar
# 碎片治理:每 5 次 Full GC 做一次压缩整理
java -XX:+UseConcMarkSweepGC \
-XX:CMSFullGCsBeforeCompaction=5 \
-XX:+UseCMSCompactAtFullCollection \
-XX:+PrintGCDetails -jar app.jar
# 观察碎片与失败:搜索日志中的关键字
grep -E "concurrent mode failure|promotion failed|ParNew" gc.log | tail -20
关键指标是「老年代使用率」与「并发模式失败次数」。一旦日志频繁出现 concurrent mode failure,说明收集器已经追不上分配速率,光调参数救不了,必须换 G1 或优化对象分配。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| CMS 用什么算法 | 标记-清除,所以不移动对象、不压缩 |
| 为什么 CMS 不压缩 | 压缩需要移动对象并更新所有引用,必须 STW,与「低停顿」目标冲突 |
| 重新标记为什么可能很长 | 要扫描脏卡与增量更新队列;新生代对象多、引用修改多时明显变慢 |
| CMS 与 G1 的标记差异 | CMS 用增量更新(写后屏障记新增),G1 用 SATB(写前屏障记删除) |
| CMS 现在还能用吗 | JDK 9 标记废弃、JDK 14 移除;只在老版本 JDK 8 环境里还能见到 |
小结
CMS 的价值在于证明了「大部分标记工作可以并发做」,优势是停顿短,代价是碎片与浮动垃圾。答这一题要能讲出「四个阶段 + 增量更新 + 两个代价」,就足够深入了。