[{"data":1,"prerenderedAt":208},["ShallowReactive",2],{"blog-java-gc":3},{"id":4,"title":5,"body":6,"category":196,"date":197,"description":16,"extension":198,"meta":199,"navigation":200,"path":201,"seo":202,"stem":203,"tags":204,"__hash__":207},"blog\u002Fblog\u002Fjava-gc.md","Java GC演进史：从CMS的妥协到ZGC的极致",{"type":7,"value":8,"toc":188},"minimark",[9,13,17,22,35,42,62,72,76,83,103,120,124,127,133,143,161,171,174,181],[10,11,5],"h1",{"id":12},"java-gc演进史从cms的妥协到zgc的极致",[14,15,16],"p",{},"回首敲代码的这十几年，Java 工程师的日常似乎总伴随着与 JVM 的相爱相杀。早年间，调优 JVM 很多时候是在和 CMS 的碎片化作斗争；而今天，随着大内存时代的到来，ZGC 已经能将停顿时间压榨到亚毫秒级。",[18,19,21],"h2",{"id":20},"_1-为什么我们抛弃了-cms","1. 为什么我们抛弃了 CMS？",[14,23,24,25,29,30,34],{},"很多年轻的程序员可能没有经历过被 CMS 的 ",[26,27,28],"code",{},"Concurrent Mode Failure"," 支配的恐惧。CMS（Concurrent Mark Sweep）是 JVM 迈向低延迟的第一步，它的核心思想是",[31,32,33],"strong",{},"并发收集","，让垃圾回收线程和用户线程尽量同时运行。",[14,36,37,38,41],{},"但这是一种",[31,39,40],{},"充满妥协","的设计：",[43,44,45,52],"ul",{},[46,47,48,51],"li",{},[31,49,50],{},"碎片化："," CMS 基于“标记-清除”算法。这意味着老年代在回收后会产生大量内存碎片。",[46,53,54,57,58,61],{},[31,55,56],{},"致命的退化："," 当碎片化严重到无法分配大对象，或者对象晋升老年代速度快于清理速度时，CMS 会直接退化为 ",[26,59,60],{},"Serial Old"," —— 也就是极其可怕的单线程 STW 全局回收。在线上几十 GB 的堆内存下，一次退化可能导致服务卡顿十几秒甚至分钟级，这对于核心在线系统是灾难性的。",[14,63,64,67,68,71],{},[31,65,66],{},"小结："," CMS 的本质是“以空间换时间”，用额外的 CPU 和内存碎片来换取短时间的暂停。它的失败在于",[31,69,70],{},"无法提供可预期的停顿时间","，这在现代高并发微服务架构下是不可接受的。",[18,73,75],{"id":74},"_2-g1-的破局化整为零与可预测停顿","2. G1 的破局：化整为零与可预测停顿",[14,77,78,79,82],{},"为了解决 CMS 的痛点，G1（Garbage-First）横空出世，并在 JDK 9 成为默认 GC。G1 的设计思路上有一次根本性的范式转移：",[31,80,81],{},"打破了物理上的年轻代与老年代隔离","。",[43,84,85,91,97],{},[46,86,87,90],{},[31,88,89],{},"Region 化设计："," G1 将堆内存划分成多个大小相等的 Region。逻辑上它们依然区分 Eden、Survivor 和 Old，但物理上不再连续。",[46,92,93,96],{},[31,94,95],{},"局部复制，消除碎片："," G1 的回收过程（Evacuation）是将存活对象从一个 Region 复制到另一个空的 Region。这种基于“标记-整理（复制）”的做法，天然避免了内存碎片问题。",[46,98,99,102],{},[31,100,101],{},"价值优先（Garbage-First）："," G1 会维护一个优先列表，每次优先回收那些“垃圾最多”的 Region，从而在有限的时间内获取最大的内存收益。",[14,104,105,107,108,111,112,115,116,119],{},[31,106,66],{}," G1 最伟大的贡献是引入了 ",[31,109,110],{},"停顿时间模型（Pause Prediction Model）","。你可以通过 ",[26,113,114],{},"-XX:MaxGCPauseMillis"," 设定一个期望的停顿时间（比如 200ms）。G1 并不追求极致的低延迟，而是追求",[31,117,118],{},"在可控的延迟下，尽可能保证高吞吐量","。它是一种极其优秀的工程折中方案。",[18,121,123],{"id":122},"_3-zgc-的降维打击指针的魔法","3. ZGC 的降维打击：指针的魔法",[14,125,126],{},"如果说 G1 是一次优秀的工程重构，那 ZGC（Z Garbage Collector）就是一次底层原理的降维打击。随着大数据和云原生的发展，堆内存动辄几百 GB 甚至 TB 级。G1 在进行对象转移（Evacuation）时，依然需要 STW，转移的对象越多，停顿越长。",[14,128,129,130,82],{},"ZGC 的核心目标只有一个：",[31,131,132],{},"在任意堆内存大小下，将 STW 时间控制在 1ms 以内（JDK 16+）",[14,134,135,136,139,140,82],{},"它是怎么做到的？核心在于两点：",[31,137,138],{},"染色指针（Colored Pointers）"," 和 ",[31,141,142],{},"读屏障（Load Barrier）",[43,144,145,151],{},[46,146,147,150],{},[31,148,149],{},"染色指针："," 传统的 GC 将标记信息存储在对象头中。而 ZGC 直接修改了对象的内存地址（指针），借用了 64 位指针中的几个比特位来标记对象的状态（是否被移动过、是否存活等）。",[46,152,153,156,157,160],{},[31,154,155],{},"并发转移与读屏障："," 当 GC 正在并发转移对象，而用户线程正好想要读取这个对象时，",[31,158,159],{},"读屏障","会被触发。它会检查指针颜色，如果发现对象已经被转移，读屏障会“自愈（Self-Healing）”这个指针，将其指向新的地址，然后再返回给用户线程。",[14,162,163,165,166,170],{},[31,164,66],{}," ZGC 将 GC 的负担从“STW 暂停”转移到了“每一次对象访问的微小 CPU 开销”上。这再次印证了架构设计的名言：",[167,168,169],"em",{},"There is no silver bullet."," ZGC 牺牲了大约 5%-10% 的极限吞吐量，换来了与堆大小完全无关的极致低延迟。",[18,172,173],{"id":173},"总结与思考",[14,175,176,177,180],{},"Java GC 的演进史，其实是一部",[31,178,179],{},"逐渐将 STW 时间从与堆大小正相关，剥离为与堆大小无关","的历史。CMS 试图并发清理，但败给了内存碎片；G1 通过化整为零的 Region 和复制算法，实现了停顿时间的可预测；而 ZGC 更是通过染色指针和读屏障，把最耗时的对象转移过程也并发化了，实现了真正的极致低延迟。在做线上技术选型时，如果是对延迟极其敏感的核心 C 端接口，我会倾向于推 ZGC；如果是后台批处理、数据清洗等看重吞吐量的任务，调优良好的 Parallel 甚至 G1 依然是好选择。",[14,182,183],{},[184,185,187],"a",{"href":186},"\u002Fblog\u002F","返回博客列表",{"title":189,"searchDepth":190,"depth":190,"links":191},"",2,[192,193,194,195],{"id":20,"depth":190,"text":21},{"id":74,"depth":190,"text":75},{"id":122,"depth":190,"text":123},{"id":173,"depth":190,"text":173},"Java底层","2026-03-15","md",{},true,"\u002Fblog\u002Fjava-gc",{"title":5,"description":16},"blog\u002Fjava-gc",[205,206],"JVM","GC","s8JgR-5z--DyrC_s22qvXE7tgReBPF9c1F-m0jgfAWI",1779959653063]