Java GC演进史:从CMS的妥协到ZGC的极致
Java GC演进史:从CMS的妥协到ZGC的极致
回首敲代码的这十几年,Java 工程师的日常似乎总伴随着与 JVM 的相爱相杀。早年间,调优 JVM 很多时候是在和 CMS 的碎片化作斗争;而今天,随着大内存时代的到来,ZGC 已经能将停顿时间压榨到亚毫秒级。
1. 为什么我们抛弃了 CMS?
很多年轻的程序员可能没有经历过被 CMS 的 Concurrent Mode Failure 支配的恐惧。CMS(Concurrent Mark Sweep)是 JVM 迈向低延迟的第一步,它的核心思想是并发收集,让垃圾回收线程和用户线程尽量同时运行。
但这是一种充满妥协的设计:
- 碎片化: CMS 基于“标记-清除”算法。这意味着老年代在回收后会产生大量内存碎片。
- 致命的退化: 当碎片化严重到无法分配大对象,或者对象晋升老年代速度快于清理速度时,CMS 会直接退化为
Serial Old—— 也就是极其可怕的单线程 STW 全局回收。在线上几十 GB 的堆内存下,一次退化可能导致服务卡顿十几秒甚至分钟级,这对于核心在线系统是灾难性的。
小结: CMS 的本质是“以空间换时间”,用额外的 CPU 和内存碎片来换取短时间的暂停。它的失败在于无法提供可预期的停顿时间,这在现代高并发微服务架构下是不可接受的。
2. G1 的破局:化整为零与可预测停顿
为了解决 CMS 的痛点,G1(Garbage-First)横空出世,并在 JDK 9 成为默认 GC。G1 的设计思路上有一次根本性的范式转移:打破了物理上的年轻代与老年代隔离。
- Region 化设计: G1 将堆内存划分成多个大小相等的 Region。逻辑上它们依然区分 Eden、Survivor 和 Old,但物理上不再连续。
- 局部复制,消除碎片: G1 的回收过程(Evacuation)是将存活对象从一个 Region 复制到另一个空的 Region。这种基于“标记-整理(复制)”的做法,天然避免了内存碎片问题。
- 价值优先(Garbage-First): G1 会维护一个优先列表,每次优先回收那些“垃圾最多”的 Region,从而在有限的时间内获取最大的内存收益。
小结: G1 最伟大的贡献是引入了 停顿时间模型(Pause Prediction Model)。你可以通过 -XX:MaxGCPauseMillis 设定一个期望的停顿时间(比如 200ms)。G1 并不追求极致的低延迟,而是追求在可控的延迟下,尽可能保证高吞吐量。它是一种极其优秀的工程折中方案。
3. ZGC 的降维打击:指针的魔法
如果说 G1 是一次优秀的工程重构,那 ZGC(Z Garbage Collector)就是一次底层原理的降维打击。随着大数据和云原生的发展,堆内存动辄几百 GB 甚至 TB 级。G1 在进行对象转移(Evacuation)时,依然需要 STW,转移的对象越多,停顿越长。
ZGC 的核心目标只有一个:在任意堆内存大小下,将 STW 时间控制在 1ms 以内(JDK 16+)。
它是怎么做到的?核心在于两点:染色指针(Colored Pointers) 和 读屏障(Load Barrier)。
- 染色指针: 传统的 GC 将标记信息存储在对象头中。而 ZGC 直接修改了对象的内存地址(指针),借用了 64 位指针中的几个比特位来标记对象的状态(是否被移动过、是否存活等)。
- 并发转移与读屏障: 当 GC 正在并发转移对象,而用户线程正好想要读取这个对象时,读屏障会被触发。它会检查指针颜色,如果发现对象已经被转移,读屏障会“自愈(Self-Healing)”这个指针,将其指向新的地址,然后再返回给用户线程。
小结: ZGC 将 GC 的负担从“STW 暂停”转移到了“每一次对象访问的微小 CPU 开销”上。这再次印证了架构设计的名言:There is no silver bullet. ZGC 牺牲了大约 5%-10% 的极限吞吐量,换来了与堆大小完全无关的极致低延迟。
总结与思考
Java GC 的演进史,其实是一部逐渐将 STW 时间从与堆大小正相关,剥离为与堆大小无关的历史。CMS 试图并发清理,但败给了内存碎片;G1 通过化整为零的 Region 和复制算法,实现了停顿时间的可预测;而 ZGC 更是通过染色指针和读屏障,把最耗时的对象转移过程也并发化了,实现了真正的极致低延迟。在做线上技术选型时,如果是对延迟极其敏感的核心 C 端接口,我会倾向于推 ZGC;如果是后台批处理、数据清洗等看重吞吐量的任务,调优良好的 Parallel 甚至 G1 依然是好选择。