CMS随记
发布时间:2026/10/4 7:00:55 作者:尧图编辑部 阅读量:1,286

一、CMS 是怎么诞生的我们前面讲过早期 JVM 的垃圾回收比较简单。例如一个比较典型的思路Java程序运行 ↓ 不断创建对象 ↓ 堆空间越来越满 ↓ 触发GC ↓ 把垃圾找出来 ↓ 回收垃圾问题在于GC 的时候JVM 到底能不能让你的 Java 程序继续运行早期很多垃圾收集器会采用一种比较简单的方式Java业务线程 ↓ 停止 ↓ GC开始 ↓ 垃圾回收 ↓ GC结束 ↓ Java业务线程继续运行这个东西叫STWStop The World也就是垃圾回收的时候把应用程序线程暂停。二、为什么 STW 会成为问题假设你有一个 Java Web 服务用户 ↓ HTTP请求 ↓ Java程序 ↓ Controller ↓ Service ↓ 数据库正常情况下请求1 → 正常处理 请求2 → 正常处理 请求3 → 正常处理 请求4 → 正常处理但是突然发生一次 Full GC请求1 → 暂停 请求2 → 暂停 请求3 → 暂停 请求4 → 暂停 ↓ GC ↓ 几百毫秒 ↓ GC结束 请求继续处理如果 GC 停顿时间比较长用户请求 ↓ 等待 ↓ 等待 ↓ 等待 ↓ 服务器终于恢复用户就会感觉这个接口怎么突然卡住了如果是普通程序停几百毫秒可能问题不大。但是如果是电商 支付 订单 游戏 高并发接口那么 GC 停顿就可能成为比较严重的问题。三、所以 CMS 想解决什么问题CMS 的全称Concurrent Mark Sweep也就是并发标记-清除这里最重要的是两个字Concurrent并发它诞生的核心目的就是减少老年代垃圾回收时的停顿时间。也就是说以前 Java线程 ↓ 停止 ↓ GC ↓ 继续运行CMS 希望变成Java线程 ─────────────→ 继续运行 ↑ │ GC │ 尽量并发执行也就是说让 GC 线程和用户线程尽可能同时工作。四、CMS 最核心的思想CMS 最重要的思想其实就一句话垃圾回收的时候不要把用户线程全部停下来。所以它采用并发标记 并发清除例如Java业务线程 ███████████████████████ CMS线程 █████████████████两个线程尽量同时运行。但是注意CMS 并不是整个过程都不 STW。CMS 依然有两个阶段需要暂停用户线程初始标记 重新标记所以 CMS 的特点不是完全没有 STW而是把 STW 尽可能缩短。五、CMS 的工作流程CMS 最经典的工作流程可以记成初始标记 ↓ 并发标记 ↓ 重新标记 ↓ 并发清除英文Initial Mark ↓ Concurrent Mark ↓ Remark ↓ Concurrent Sweep你一定要记住初始标记和重新标记需要 STW。而并发标记和并发清除可以和用户线程同时运行。六、第一阶段初始标记首先是Initial Mark初始标记这个阶段会暂停用户线程。为什么因为 CMS 要快速找到GC Roots 直接引用的对象。例如GC Root ↓ User ↓ Order ↓ Address初始标记主要先把GC Roots ↓ 直接关联对象标记出来。所以用户线程暂停 CMS线程 进行标记这个阶段虽然 STW但是一般时间比较短。因为它主要做的是找到 GC Roots 直接关联的对象。七、第二阶段并发标记初始标记结束Java线程恢复然后 CMS 开始Concurrent Mark并发标记这个阶段非常重要。CMS 会沿着前面找到的对象继续往下找GC Roots ↓ A ↓ B ↓ C ↓ D不断遍历整个对象引用关系。例如GC Root ↓ User ↓ Order ↓ Product ↓ Category这些都可能被标记为存活对象。而这个阶段Java业务线程继续运行 CMS线程 继续标记也就是说Java线程 ███████████████████████ CMS线程 ███████████████████两者同时执行。这就是 CMS 中Concurrent并发最核心的体现。八、但是这里出现了一个非常麻烦的问题这也是理解 CMS 的关键。假设 CMS 正在标记A → B → C这个时候Java业务线程突然修改了对象关系A → B原来的A → B → C变成A → B C或者原来A BJava线程突然A → B这时候CMS 在并发标记的时候Java线程也在修改对象引用关系。所以 CMS 不能简单地标记完 ↓ 直接清除否则可能出现漏标或者错标。所以 CMS 后面还需要一个非常重要的阶段重新标记Remark九、第三阶段重新标记重新标记Remark这个阶段又会STW。也就是Java业务线程暂停 CMS线程 重新检查它的作用就是把并发标记过程中由于用户线程继续运行而产生的引用变化重新检查一遍。可以理解成并发标记 ↓ Java线程也在修改对象 ↓ 可能产生漏标 ↓ 重新标记 ↓ 把这些变化重新处理所以 CMS初始标记 → STW 并发标记 → 不STW 重新标记 → STW 并发清除 → 不STW这是 CMS 最重要的一张图CMS 初始标记 ↓ 【STW】 ↓ 并发标记 ↓ 【用户线程继续】 ↓ 重新标记 ↓ 【STW】 ↓ 并发清除 ↓ 【用户线程继续】十、第四阶段并发清除重新标记完成之后CMS 已经基本确定哪些对象是垃圾。接下来Concurrent Sweep并发清除开始清除垃圾对象。例如[A][垃圾][B][垃圾][C][垃圾]清除之后[A][空][B][空][C][空]而这个时候Java业务线程继续运行 CMS线程 清理垃圾所以又是Java线程 ███████████████████ CMS线程 ███████████████十一、为什么 CMS 使用“标记-清除”你前面已经学过标记-清除会产生内存碎片。CMS 为什么还要这么干因为 CMS 的目标不是最大程度整理内存。而是尽可能降低 GC 停顿时间。如果使用标记-整理标记 ↓ 寻找存活对象 ↓ 移动对象 ↓ 整理内存移动大量对象本身就比较耗时而且对象移动过程中还会涉及引用关系处理。CMS 更希望标记 ↓ 找到垃圾 ↓ 直接清除这样可以减少停顿。所以 CMS 的设计思想就是宁愿产生一些内存碎片也要尽量减少 GC 停顿时间。十二、但是 CMS 也有一个很大的问题内存碎片因为 CMS 使用标记-清除所以清理之后[A][空][B][空][C][空][D][空]时间长了之后内存碎片 ↓ 越来越多最终可能出现老年代还有很多空闲空间 但是 没有足够大的连续空间这时候如果来了一个大对象大对象可能就无法分配。于是 CMS 可能触发Concurrent Mode Failure然后退化成一次Full GC这次 Full GC 往往就需要更长时间的 STW。所以 CMS 虽然解决了长时间GC停顿但是又带来了内存碎片 CPU资源竞争 并发标记复杂性 Concurrent Mode Failure十三、CMS 还有一个重要问题浮动垃圾这个也建议你理解一下。因为 CMS 是并发标记也就是说CMS在标记 Java程序还在运行假设CMS开始标记 A → B此时 Java 程序又创建了一个对象C或者某个对象原本还活着A → B后来 Java 程序修改引用A不再引用B那么在 CMS 这一次 GC 中有一些垃圾对象可能没有被及时回收。这些垃圾对象就叫浮动垃圾Floating Garbage它们不会影响程序正确运行。只是这次GC先不回收下一次GC再回收。所以 CMS 需要预留一定空间让这些浮动垃圾暂时存在。十四、CMS 为什么会出现 CPU 压力因为Java业务线程 CMS垃圾回收线程是同时运行的。例如CPU 业务线程 → 使用CPU CMS线程 → 使用CPU所以 CMS 虽然减少了STW时间但是并不是没有成本。它实际上是用一部分CPU资源换取更低的GC停顿时间。这个思想你一定要理解。十五、CMS 到底解决了什么问题现在可以总结了。在 CMS 出现之前传统垃圾回收器面对老年代回收时老年代满了 ↓ STW ↓ 垃圾回收 ↓ STW时间可能比较长对于高并发 低延迟 Web服务这是一个比较大的问题。CMS 出现之后老年代GC 初始标记 ↓ 短暂STW 并发标记 ↓ 业务线程继续运行 重新标记 ↓ 短暂STW 并发清除 ↓ 业务线程继续运行所以 CMS 的核心目标就是降低老年代垃圾回收造成的停顿时间。十六、你可以用一个生活中的例子理解 CMS假设你在图书馆工作。传统 GC图书馆营业 ↓ 突然关门 ↓ 所有人出去 ↓ 工作人员整理垃圾 ↓ 整理完成 ↓ 重新开门这就是STWCMS图书馆继续营业 ↓ 工作人员一边整理 ↓ 顾客继续看书 ↓ 工作人员发现变化 ↓ 短暂暂停营业 ↓ 重新确认 ↓ 继续营业所以 CMS 的思想不是完全停止营业。而是让整理工作尽可能和正常营业同时进行。