CMS(Concurrent Mark-Sweep)是 Java 第一个真正意义上的并发收集器,它把大部分标记工作放到与应用线程并发执行,大幅缩短了停顿时间。本文分析 CMS 的四个阶段、两大缺陷,以及为何最终被 G1 取代。
目录
| 章节 | 说明 |
|---|---|
| 设计目标 | 低延迟优先,牺牲吞吐量 |
| 四个阶段 | 初始标记→并发标记→重新标记→并发清除 |
| 两大缺陷 | 浮动垃圾与内存碎片 |
| 并发失败与降级 | Concurrent Mode Failure |
| CMS 的历史地位 | 过渡性角色 |
设计目标
在 CMS 之前(Serial Old、Parallel Old),老年代 GC 是完全 STW 的——应用线程全部暂停,GC 线程独占 CPU 扫描整个老年代,停顿时间可达数秒。
CMS 的核心思想: 把耗时最长的标记和清除工作拆分,让大部分工作与应用线程并发执行,只在必要时短暂 STW。
四个阶段
阶段一:初始标记(Initial Mark)⏸️ STW
做什么: 标记 GC Roots 直接可达的对象(第一层)
为什么需要 STW: 扫描线程栈帧、静态变量等 GC Roots 时,如果应用线程同时在运行(方法调用、引用赋值),GC Roots 集合一直在变化,无法获取一致快照,会导致漏标对象。初始标记虽然只扫描第一层,但必须是原子性的。
特点: 停顿极短(毫秒级),因为只扫描 GC Roots 直接引用,不追踪整个对象图。
阶段二:并发标记(Concurrent Mark)🔄 并发
做什么: 从初始标记的对象出发,并发地遍历整个对象图
特点: 耗时最长,但与应用并发,不 STW。问题:应用线程可能在此期间修改引用关系,导致标记不准确(需要第三阶段修正)。
阶段三:重新标记(Remark)⏸️ STW
做什么: 修正并发标记期间因应用线程运行而导致的标记变动,使用增量更新(Incremental Update) 算法
为什么需要 STW: 处理脏引用时,如果应用线程同时还在产生新的引用变化,会形成无限追逐——新变化不断写入脏集合,永远无法处理完毕。STW 保证了处理期间不再有新变化,使修正可以在有限时间内完成。
并发标记期间 GC Roots 的变化如何处理:
特点: STW,比初始标记慢(需要扫描整个年轻代 + 脏对象),是 CMS 停顿最长的阶段,但远比 Full GC 短。
阶段四:并发清除(Concurrent Sweep)🔄 并发
做什么: 并发地清除未标记(垃圾)对象,回收内存
特点: 使用空闲列表管理回收空间(不整理,不移动对象,避免更新引用的开销),不 STW。
两大缺陷
缺陷一:浮动垃圾(Floating Garbage)
并发标记和并发清除期间,应用线程仍在运行,可能产生新的垃圾:
-XX:CMSInitiatingOccupancyFraction=68 # 可调低触发阈值,默认 92%
缺陷二:内存碎片
CMS 使用标记-清除(不是标记-整理),清除后不移动对象,产生大量碎片:
并发失败与降级
Concurrent Mode Failure: CMS 并发收集期间,老年代已经被填满,来不及完成 GC。
原因:
- 老年代填满速度过快(大量对象晋升)
- 浮动垃圾积累
-XX:CMSInitiatingOccupancyFraction设置过高
后果:
- CMS 暂停并发 GC
- 启用 Serial Old 收集器做 Full GC(单线程,整理内存)
- 停顿时间从毫秒级升至秒级甚至分钟级
这是 CMS 最严重的问题,在高负载下难以避免。
CMS 的历史地位
CMS 在 JDK 5 引入,JDK 9 被标记为废弃,JDK 14 被彻底移除。
贡献:
- 证明了并发 GC 的可行性,大幅降低了 GC 停顿
- 推动了低延迟 GC 的研究,为 G1、ZGC 铺路
- 在其活跃的十年间,是低延迟场景的标准选择
局限:
- 内存碎片问题无法根治(不整理)
- 并发失败时降级代价巨大
- 无法精确预测停顿时间
- 老年代碎片化导致分配效率越来越低
G1 的设计目标正是解决 CMS 的这些问题:通过 Region 化堆布局同时做到低延迟和可预测停顿。
CMS 适用场景(历史参考)
- 老年代大,Full GC 代价高
- 延迟敏感(互联网服务、实时系统)
- JDK 7/8 时代,G1 还不成熟时的最优选
参考资料
- 《深入理解 Java 虚拟机》第 3 章 — 周志明
- JEP 291: Deprecate the Concurrent Mark Sweep (CMS) GC
- G1 收集器(CMS 的继任者)
评论 (0)
发表评论