ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

08-ZGC 与 Shenandoah:亚毫秒延迟

08-ZGC 与 Shenandoah:亚毫秒延迟 本篇是「JVM 与性能调优系列」第 8 篇系列重点。G1 把停顿压到几百毫秒ZGC 直接压到10ms 以内且无论堆多大。它是怎么在回收时让业务线程几乎无感的两个杀器染色指针 读屏障。一、为什么还要 ZGCG1 已经很好但有个硬伤停顿仍随堆增大而增长。Mixed GC 要扫描、回收一批 Region堆越大、老年代越多单次停顿越长。对交易、风控、实时竞价这类系统任何超过几十毫秒的停顿都可能造成资损或超时。它们要的是「无论堆是 8G 还是 8T停顿都 10ms」。这是 G1 给不了的于是 ZGCJDK 11 实验、JDK 15 生产可用和 RedHat 的 Shenandoah 登场了。二、ZGC 的核心指标ZGC 最震撼的一点是停顿只与「根集合GC Roots大小」有关与堆大小、存活对象数量无关。根集合栈、寄存器、全局变量指向的对象通常很小且增长缓慢所以无论堆里躺着 10 亿还是 100 亿个对象ZGC 的停顿都稳定在极低水平。这彻底打破了「堆越大停顿越长」的魔咒。三、染色指针Colored Pointers这是 ZGC 的灵魂。64 位系统中x86-64 实际只用低 44 位寻址支持 16TB高 20 位没用。ZGC 把对象的一些状态信息直接编码进指针本身47..44 位预留 43 位Finalizable可终结 42 位Remapped重映射 41 位Marked1标记1 40 位Marked0标记0 39..0 位对象实际地址也就是说一个「指针」里同时携带了「这个对象是否被标记、是否要被重映射、是否可终结」的信息。不需要去对象头里翻状态看指针就知道。因为信息在指针上对象搬移到新地址后只需更新指针的颜色位引用自然跟上——这是后续「并发搬迁」能实现的前提。四、读屏障Load Barrier有了染色指针还需要一个机制在「读取引用」时顺便做点事。ZGC 在每次从堆里读取对象引用的地方插入一段极小的「读屏障」// 伪代码读对象引用时Object pobj.field;// 这里被插入读屏障if(color(p)need_remap)premap(p);// 顺手修正读屏障检查指针的颜色如果对象已被搬迁就顺手把它重定向到新地址。因为读操作远多于写操作用读屏障而非 G1 的写屏障能覆盖更全、且几乎不阻塞业务线程。代价每次读引用都有微小开销所以 ZGC 在吞吐上略逊于 G1通常差个位数百分比这是为极低延迟付出的合理代价。五、ZGC 的并发阶段与转发表ZGC 的回收几乎全并发关键阶段并发标记用染色指针 读屏障标记存活对象与业务并发并发预备重分配确定要回收的 Region并发重映射Relocation把存活对象搬到新 Region难点在于对象搬走了旧地址还有别人引用怎么办ZGC 用转发表Forwarding Table旧地址 → 新地址的映射。业务线程读旧引用时读屏障发现需要 remap就从转发表拿到新地址透明地访问新对象——业务线程全程无感。等所有旧引用都通过读屏障更新完毕转发表就可以回收了。六、Shenandoah 对比同赛道的 ShenandoahRedHat 主导OpenJDK 集成目标一致亚毫秒停顿、停顿与堆无关。实现路线不同Shenandoah 用「Brooks 指针」在对象头里额外加一个转发指针forwarding pointer对象搬走后通过头里的指针找到新位置ZGC 用「染色指针」状态在指针上不必动对象头优劣Brooks 指针每次访问要多一次间接跳转但实现相对直观染色指针更巧妙、吞吐略好但依赖特定 CPU 位布局需 64 位、限制堆上限约 4TB~16TB对绝大多数场景足够。两者都证明了「并发压缩 读屏障」路线能达成亚毫秒停顿。七、ZGC 实战参数-XX:UseZGC-XX:SoftMaxHeapSize32g# 软上限GC 尽量把堆压到这以下-Xmx48g# 硬上限-Xlog:gc*# JDK11 统一日志框架关键点ZGC 的强项在大堆64G 到数 T堆太小反而体现不出优势还白费读屏障开销SoftMaxHeapSize让 ZGC 在空闲时主动归还内存给 OS比 G1 更省需要较新 JDK生产推荐 JDK 17/21ZGC 已成熟稳定八、适用边界ZGC 不是银弹小堆 吞吐优先的业务Parallel 或 G1 可能吞吐更高、更省心升级到支持 ZGC 的 JDK 有迁移成本依赖、测试监控体系要跟上确认停顿确实按预期用-Xlog:gc*看 pause 时长但如果你受够了 Full GC 的长停顿、或业务对延迟极度敏感ZGC 是目前最优雅的答案。九、读懂 ZGC 的 GC 日志ZGC 的日志信息量很足-Xlog:gc*下你会看到类似[gc,start] GC(0) Pause Mark Start [gc,phase] GC(0) Concurrent Mark 12.3ms [gc,phase] GC(0) Concurrent Relocate 8.1ms [gc,pause] GC(0) Pause Total 1.2ms注意Pause Total通常只有个位数毫秒而且和堆大小无关——这就是 ZGC 的卖点。验证 ZGC 是否达标就看这个Pause行如果某次突然几百毫秒说明触发了某种 Stop-The-World 兜底极少但监控要盯。十、ZGC vs G1 选型决策维度G1ZGC停顿几十~几百 ms10ms与堆无关吞吐高略高于 ZGC略低读屏障开销堆大小数百 M~数十 G数 G~数 T 优势明显JDK 要求817/21 最佳调优难度中等参数多低几乎只管堆决策堆 16G 且能忍百毫秒级停顿 → G1 省心堆大或延迟敏感 → ZGC。两者都不是错误选择只是取舍不同。十一、迁移到 ZGC 的注意事项JDK 版本生产推荐 17/21 LTSZGC 在这些版本已稳定别在 JDK 11 上硬上仍实验性依赖兼容升级 JDK 可能碰到第三方库不兼容先在测试环境全量回归监控适配ZGC 的 MBean/指标名称与 G1 不同Grafana 面板要相应调整别指望吞吐更高ZGC 赢在延迟吞吐通常略低于 G1预期管理要做好十二、ZGC 的多重映射与颜色指针的代价染色指针有个实现细节值得提为了把「带颜色位的指针」既能当真实地址用、又能当标记信息读ZGC 用了多重映射Multi-Mapping——把同一段物理内存映射到三个不同的虚拟地址区间每个区间对应一种颜色位组合。读指针时按当前阶段选对应映射从而同时拿到「地址」和「状态」。代价与限制颜色位占用了高 4 位理论上限了堆地址空间约 4TB~16TB实际远超需求多重映射增加了虚拟地址空间占用用虚拟换便利物理不变读屏障在每次对象引用加载时插入是 ZGC 吞吐略低于 G1 的根源理解这些你就不会把 ZGC 神化——它是「用一点空间/吞吐换极低延迟」的精巧权衡。十三、ZGC 与大对象、NUMA两个实战细节大对象友好Humongous 对象在 ZGC 里不像 G1 那样容易引发 early GC因为并发搬迁不受 Region 大小束缚大对象也能平滑移动NUMA 感知ZGC 支持 NUMA非统一内存访问会把对象分配在访问它的 CPU 本地内存上多路服务器上能进一步降延迟。开启-XX:UseNUMA需硬件支持这些细节说明 ZGC 在设计上就为「大堆、低延迟、现代多路硬件」而生。十四、什么场景不该用 ZGC堆 8G 且吞吐至上G1/Parallel 更省心ZGC 的读屏障开销不划算JDK 无法升级ZGC 在老 JDK 上要么没有要么实验性强上风险大监控体系缺失ZGC 日志和 MBean 与 G1 不同没配套的 Grafana 面板出问题你眼睛是瞎的技术选型没有银弹ZGC 是「低延迟场景的银弹」但不是「所有场景的银弹」。总结ZGC 用染色指针状态编码进 64 位指针读屏障读引用时顺手修正转发转发表旧地址透明映射到新地址实现了「停顿与堆大小无关」的亚毫秒回收。它把并发压缩做到了极致是 JVM 低延迟工程的巅峰。代价是轻微吞吐损失和较高 JDK 版本要求。下一篇我教你读 GC 日志、把参数定下来。
返回列表