深入解密 JVM:CMS 垃圾回收器的“并发标记”到底是不是多此一举?

张开发
2026/4/9 10:50:12 15 分钟阅读

分享文章

深入解密 JVM:CMS 垃圾回收器的“并发标记”到底是不是多此一举?
深入解密 JVMCMS 垃圾回收器的“并发标记”到底是不是多此一举在学习 JVM 垃圾回收机制时很多开发者在看到 CMS (Concurrent Mark Sweep) 垃圾回收器的执行步骤图时都会产生一个直击灵魂的疑问“初始标记和重新标记不是已经把所有的存活对象都标记过了吗为什么中间还要夹一个耗时极长的‘并发标记’这到底有什么用”这是一个非常牛的观察很多初学者都会被这几个名字给“骗”了。你之所以会产生这个巨大的逻辑冲突是因为你严重高估了“初始标记Initial Mark”的工作量直接给你纠正这个极其关键的底层误区初始标记根本就没有把所有的存活对象标记出来它仅仅只标记了冰山一角。为了让你彻底看清这三个阶段到底在干嘛我们把整个标记存活对象的过程拆解为“1%、98%、1%”的工作量分配比例来看。一、 初始标记Initial Mark只干 1% 的活必须 STW真相揭秘在这个阶段GC 线程绝对不会顺藤摸瓜去遍历整个堆内存里的所有对象。它只做极其简单的一件事仅仅标记那些和 GC Roots 直接相连的“第一层对象”。为什么这么快因为程序中作为 GC Roots 的对象比如线程栈里的局部变量、静态变量以及它们的直系子对象数量是极少的。所以虽然这个阶段需要“暂停所有用户线程Stop The World, STW”但由于工作量极小暂停时间极其短暂你几乎感觉不到系统的卡顿。二、 并发标记Concurrent Mark干 98% 的脏活累活核心作用这才是真正干大活的阶段也是图示中最核心的一步。GC 线程会从刚才“初始标记”找出的第一层对象开始顺藤摸瓜一层一层地深度遍历整个极其庞大的对象关系引用图可能有几十万、几百万个对象。为什么必须“并发”因为遍历所有的存活对象实在太耗时了如果这个时候让系统停下来STW去慢慢找你的 Java 程序可能会直接卡顿好几秒甚至几十秒这对于 Web 应用来说是灾难性的。为了不让你觉得卡CMS 做出了最大的让步让 GC 线程和你的业务用户线程同时运行并发。三、 重新标记Remark查漏补缺的最后 1%必须 STW真相揭秘既然第二步是“并发”干活的这就意味着一个极其残酷的现实在 GC 线程到处做标记的时候你的业务线程还在疯狂地 new 新对象或者修改老对象的引用关系核心作用经过漫长的第二步GC 线程终于把对象图遍历完了。但因为业务线程在捣乱导致第二步的结果不一定 100% 准确会产生著名的“漏标”或“错标”问题。所以CMS 必须最后再咬牙暂停一次用户线程STW花极短的时间把第二步期间发生“引用变动”的那一小撮对象重新核对并修正一遍。 终极通俗大比喻警察抓黑帮网络如果上面的技术原理解释还不够直观我们来想象一个“抓出整个黑帮网络找存活对象”的场景初始标记封锁全城 1 秒钟警察突然行动直接冲进几个老大的家里把老大GC Roots 的直系小弟按住。因为目标明确耗时极短。并发标记全城恢复正常生活警察便衣暗访警察根据老大的手机通讯录顺藤摸瓜去查下面的堂主、小弟、马仔遍历整个对象图。这个过程可能要查好几个月为了不引发经济瘫痪绝对不能封锁城市必须让市民继续正常生活业务线程并发运行。重新标记再次封锁全城 1 分钟因为查了几个月这期间肯定有小弟新认了大哥或者有人退出了黑帮。为了防止抓错人或漏抓人警察必须再短暂停顿一下城市把这几个月内发生的“人事变动”对齐一下账本做最后的收网确认。总结现在你明白了如果没有图中的并发标记CMS 根本找不到那剩下 98% 藏在深处的存活对象。正因为顺藤摸瓜找对象极其耗时CMS 为了降低停顿时间Low Pause才让业务线程一起跑也正因为允许业务线程一起跑才不可避免地引出了后面那一步用于“擦屁股”和“对账”的重新标记。允许业务线程一起跑才不可避免地引出了后面那一步用于“擦屁股”和“对账”的重新标记。这就是 CMS 垃圾回收器在“吞吐量”与“低延迟”之间做出的最精妙的博弈

更多文章