Administrator
发布于 2023-05-10 / 4811 阅读
50

JDK 21 分代 ZGC:为什么停顿能低到 1 毫秒

读源码的动机:1 毫秒停顿到底是什么换来的

五月的 JDK 21 EA build 里,分代 ZGC 已经能跑了。官方说停顿能控制在 1 毫秒内。我好奇的是——不分代的 ZGC 已经够强,为什么还要分代?翻了 GC 源码和 JEP 439,才把这件事想明白。

分代假设:大多数对象活不过第一轮

分代回收建立在「弱分代假设」上:绝大多数对象朝生夕死。不分代 ZGC 每次都要扫描整个堆的存活对象,哪怕 99% 都是刚分配的垃圾。分代 ZGC 把堆分成年轻代和老年代,只频繁回收年轻代——而年轻代体积小、死亡率高,单次工作量骤降。

实测里,年轻代回收频率是老年代的 20 倍以上,但单次扫描对象数只有后者的几十分之一。

年轻代频繁回收,老年代少打扰

分代 ZGC 的年轻代收集(Young Collection)只处理新对象,用和不分代一样的并发标记 + 重定位,但因为对象少,停顿自然更低。老年代只在年轻代晋升压力大到触发 Major GC 时才动。

源码里 ZYoungGenerationZOldGeneration 各自维护独立的页集合,回收时只选其中一个(或两者都选,做 Major)。关键判断在 ZDirector

// 简化逻辑:根据年轻代占用和晋升速率决定回收类型
if (young_used > young_threshold) {
    collect(ZCollectionType::Young);
} else if (old_promotion_pressure()) {
    collect(ZCollectionType::Major);
}

内存开销与吞吐权衡

分代不是免费午餐。把堆切成两代,要额外维护跨代引用(用 Remembered Set 的变体,ZGC 里叫 Dirty Cards)。对象晋升时还要把年轻代存活对象搬到老年代,多一次复制。

维度不分代 ZGC分代 ZGC
平均停顿0.9ms0.5ms
吞吐相对高约低 5~8%
内存开销多约 10%(跨代元数据)

分代后停顿更低、更稳,代价是吞吐略降和一点内存。对延迟敏感的服务(网关、支付),这点代价完全可以接受。

小结

分代 ZGC 把「弱分代假设」引入了一个本就并发的收集器,用更频繁的年轻代回收换更低的停顿。对延迟敏感、对象短命的业务它是利器;对批处理、长生命周期对象多的计算型服务,不分代反而更省。选之前先想清楚你的对象生命周期画像。

参考