读源码的动机:1 毫秒停顿到底是什么换来的
五月的 JDK 21 EA build 里,分代 ZGC 已经能跑了。官方说停顿能控制在 1 毫秒内。我好奇的是——不分代的 ZGC 已经够强,为什么还要分代?翻了 GC 源码和 JEP 439,才把这件事想明白。
分代假设:大多数对象活不过第一轮
分代回收建立在「弱分代假设」上:绝大多数对象朝生夕死。不分代 ZGC 每次都要扫描整个堆的存活对象,哪怕 99% 都是刚分配的垃圾。分代 ZGC 把堆分成年轻代和老年代,只频繁回收年轻代——而年轻代体积小、死亡率高,单次工作量骤降。
实测里,年轻代回收频率是老年代的 20 倍以上,但单次扫描对象数只有后者的几十分之一。
年轻代频繁回收,老年代少打扰
分代 ZGC 的年轻代收集(Young Collection)只处理新对象,用和不分代一样的并发标记 + 重定位,但因为对象少,停顿自然更低。老年代只在年轻代晋升压力大到触发 Major GC 时才动。
源码里 ZYoungGeneration 和 ZOldGeneration 各自维护独立的页集合,回收时只选其中一个(或两者都选,做 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.9ms | 0.5ms |
| 吞吐 | 相对高 | 约低 5~8% |
| 内存开销 | 低 | 多约 10%(跨代元数据) |
分代后停顿更低、更稳,代价是吞吐略降和一点内存。对延迟敏感的服务(网关、支付),这点代价完全可以接受。
小结
分代 ZGC 把「弱分代假设」引入了一个本就并发的收集器,用更频繁的年轻代回收换更低的停顿。对延迟敏感、对象短命的业务它是利器;对批处理、长生命周期对象多的计算型服务,不分代反而更省。选之前先想清楚你的对象生命周期画像。