CMS 的底层原理是什么?优势在哪?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 对经典垃圾收集器的理解:面试官想确认你是否了解 CMS 收集器的核心设计思想 —— 以 “获取最短回收停顿时间” 为目标。
  2. 并发收集的底层机制:你是否清楚 CMS 是如何通过 “并发” 来降低停顿的?它的几个关键步骤分别做了什么,哪些阶段需要 Stop-The-World?
  3. 优缺点的辩证认知:不仅要知道 CMS 的优势(低延迟),更要了解它的劣势(CPU 敏感、浮动垃圾、空间碎片、并发失败),以及在生产环境中可能遇到的典型问题。
  4. 版本演进意识:面试官可能会考察你是否知道 CMS 在 JDK 9 后被废弃、JDK 14 中正式移除,以及为什么会被 G1、ZGC 取代,这反映了你对技术发展方向的把握。

核心答案

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的老年代垃圾收集器。它基于 “标记-清除” 算法实现,核心思想是尽可能让垃圾收集线程与用户线程并发执行,从而减少 Stop-The-World(STW)的时间。

它的主要优势是低停顿、高响应,适合对延迟敏感的应用。但缺点是对 CPU 资源敏感无法处理浮动垃圾会产生大量内存碎片,并且在某些情况下会退化为 Serial Old 进行 Full GC

深度解析

底层原理与工作步骤

CMS 收集器的整个生命周期可以分为六个步骤,其中初始标记和重新标记需要 STW,其余步骤可以和用户线程并发执行。

  • 步骤一:初始标记(Initial Mark)【STW】

    • 这个阶段需要暂停所有用户线程。
    • 它的任务是标记出所有从 GC Roots 直接可达的老年代对象,以及被年轻代对象引用的老年代对象(即跨代引用)。因为年轻代的对象变动频繁,如果不考虑跨代引用,可能会导致老年代对象被误回收。
    • 由于只标记直接关联的对象,所以停顿时间很短。
  • 步骤二:并发标记(Concurrent Mark)

    • 从 GC Roots 开始遍历整个老年代对象图,标记所有存活的对象。
    • 这个阶段是与应用线程并发执行的,所以不会暂停用户线程。但这也意味着在标记过程中,用户线程仍在运行,可能会改变对象的引用关系。
  • 步骤三:并发预清理(Concurrent Preclean)

    • 这是一个可选的并发阶段,目的是在重新标记之前,尽量处理掉那些在并发标记阶段因用户线程运行而发生变化的对象(例如,新生代晋升的对象、直接分配在老年代的对象,以及被并发标记线程标记为脏页的区域)。
    • 它可以减少下一个 “重新标记” 阶段的工作量,从而缩短 STW 时间。
  • 步骤四:重新标记(Remark)【STW】

    • 这个阶段会再次暂停所有用户线程,来修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
    • CMS 采用了比简单遍历更高效的算法(如增量更新),来保证这个阶段的停顿时间尽可能短。尽管如此,它仍然是 CMS 中另一次较短的停顿。
  • 步骤五:并发清除(Concurrent Sweep)

    • 与用户线程并发地清理掉那些被标记为死亡的对象,回收它们占用的内存空间。
    • 由于使用了 “标记-清除” 算法,这里不会进行压缩整理,所以会产生内存碎片。
  • 步骤六:并发重置(Concurrent Reset)

    • 并发执行,重置 CMS 收集器的内部数据结构,为下一次 GC 做准备。

核心优势

  • 低停顿:最重要的优势。通过将最耗时的并发标记和并发清除阶段与应用线程并发执行,大大减少了 STW 的时间。对于 B/S 架构的服务端应用来说,能够提供更平滑的响应时间,避免用户请求因 GC 停顿而产生明显的卡顿。

劣势与常见问题

  • 对 CPU 资源非常敏感:并发阶段虽然不会暂停用户线程,但会占用一部分 CPU 资源,导致应用吞吐量下降。在 CPU 核心数较少(比如单核)的机器上,CMS 的影响会比较明显。

  • 无法处理浮动垃圾(Floating Garbage):由于并发清理阶段用户线程仍在运行,自然就会不断产生新的垃圾。这部分垃圾出现在标记过程之后,CMS 无法在本次收集中处理它们,只能留到下一次 GC 清理。这部分 “浮动垃圾” 需要预留足够的内存空间来容纳,因此 CMS 不能像其他收集器那样等到老年代几乎填满了再回收,必须预留一部分空间供并发收集期间程序使用。JDK 1.6 版本后,CMS 的启动阈值大约在老年代92%(可以通过 -XX:CMSInitiatingOccupancyFraction 调整)。

  • “并发失败(Concurrent Mode Failure)”风险:如果 CMS 运行期间老年代预留的内存无法满足程序分配新对象的需要,就会导致“并发失败”。此时 JVM 会降级,冻结应用线程,临时启用 Serial Old 收集器进行老年代的垃圾回收(Full GC),这会导致非常长的停顿时间。

  • 内存碎片问题:基于 “标记-清除” 算法,不压缩,所以会产生大量空间碎片。当碎片过多导致无法为大对象分配连续空间时,也会触发 Full GC。可以通过 -XX:+UseCMSCompactAtFullCollection(默认开启)和 -XX:CMSFullGCsBeforeCompaction 来设置在 Full GC 时进行碎片整理,但这也会增加停顿时间。

配置示例与最佳实践

在 JDK 8 中,启用 CMS 通常配合 ParNew 新生代收集器:

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

调优建议

  • 合理设置 CMS 触发阈值:根据应用的对象分配速率调整 -XX:CMSInitiatingOccupancyFraction=N,避免并发失败。
  • 启用类卸载:如果应用需要频繁动态生成类(比如热部署),可以加上 -XX:+CMSClassUnloadingEnabled
  • 监控与日志:开启 GC 日志 -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps,以便分析 CMS 各阶段耗时及是否发生并发失败。

常见误区

  • 误区一:认为 CMS 不会 STW。实际上初始标记和重新标记两个阶段仍然需要暂停应用,只不过时间很短。
  • 误区二:盲目设置 -XX:CMSInitiatingOccupancyFraction=0 或 100。设置过低会导致频繁 GC,过高则容易引发并发失败,需要根据实际运行情况调整。
  • 误区三:忽略碎片整理的开销。在 CMS 被迫进行 Full GC 压缩时,停顿会非常长,监控中要特别关注。

CMS 的现状与替代者

JDK 9 开始,CMS 被标记为废弃(deprecated),并在 JDK 14 中正式移除。官方推荐使用 Garbage-First (G1) 收集器作为替代。G1 通过将堆划分为 Region,实现了可预测的停顿时间模型,并且能更好地避免内存碎片和并发失败问题。对于追求更低延迟的场景,还有 ZGCShenandoah 可供选择。

总结

CMS 收集器是 JVM 历史上里程碑式的低延迟收集器,它通过 “并发标记-清除” 实现了短暂的停顿,非常适合 Web 应用等对响应时间敏感的场景。但它在 CPU 资源利用、内存碎片和并发失败方面的短板,以及后续 G1、ZGC 等更先进的收集器的出现,最终让它退出了历史舞台。理解 CMS 的原理,有助于我们更深入地理解 JVM 垃圾回收的演进方向。