29-ZGC 与 Shenandoah - 低延迟收集器G1 把停顿控制在了百毫秒级对多数 Web 服务够用。但有一类场景要求个位数毫秒甚至亚毫秒停顿——高频交易、实时风控、流式计算。G1 的 STW 阶段初始标记、重新标记、整理在大堆上仍可能几十毫秒。ZGCOracle和ShenandoahRed Hat把停顿推到了亚毫秒级它们的秘密是把整理阶段也并发化。本篇深入剖析这两款当代前沿收集器的核心技术——染色指针、读屏障、转发指针并给出对比选型建议。为什么需要亚毫秒级 GCG1 的瓶颈在于对象搬运整理阶段是 STW 的——因为移动对象后要更新所有指向它的引用这个更新引用操作必须 STW 才能保证一致性。堆越大要更新的引用越多停顿越长。G1 痛点 标记 ── 已并发化SATB ✓ 清除 ── 已并发化 ✓ 整理 ── STW ✗ ← 这里是停顿根源ZGC 和 Shenandoah 的共同目标是把整理对象搬运 引用更新也并发化。两者的实现路径不同——ZGC 用染色指针 读屏障Shenandoah 用转发指针 写屏障。ZGC 核心技术染色指针ZGC 运行在 64 位系统上一个指针 64 位但实际寻址只用了低位堆通常 4TB 或 16TB。高位空闲——ZGC 拿来存GC 元数据这就是染色指针Colored Pointers。64 位指针布局ZGC4TB 堆 位: 63 45 44 43 42 41 40 16 15 0 ┌─────────┬────┬────┬────┬────┬──────┬──────────┐ │ 未使用 │ F │ M0 │ M1 │ R │ 偏移 │ 地址 │ └─────────┴────┴────┴────┴────┴──────┴──────────┘ 18位 1位 1位 1位 1位 24位 16位 染色位4 位 F Finalizable可回收标记 M0 Mark 0标记位 0 M1 Mark 1标记位 1 R Remapped重映射表示引用已修复核心思想把对象的 GC 状态是否已标记、是否需重映射直接编码进指针而不是存在对象头里。这样修改对象状态时不用动对象本身只需改指针的染色位——而改指针可以并发进行配合读屏障。传统方案GC 状态存在对象头 → 修改状态要动对象 → 要 STW ZGC 方案GC 状态存在指针 → 改状态只动指针 → 读屏障并发修复读屏障染色指针改变了对象状态的存储位置那应用线程读取引用时就要检查并修复指针——这就是读屏障。// 伪代码ZGC 读屏障由 JIT 插入对应用透明ObjectreadBarrier(Objectref){if(refnull)returnnull;if(coloredBits(ref)!expectedColor){// 指针染色不对说明对象已被移动或待修复reffixPointer(ref);// 修复为正确地址}returnref;}// 每次读引用都过读屏障Objectobjfield.bar;// JIT 插入obj readBarrier(field.bar);读屏障的代价是每次引用读取多几条指令但 JIT 优化后开销通常在 5-10%。读屏障的好处是让整理可以并发——即使对象被移动了下次读取时读屏障会自动修复指针。并发整理流程ZGC 的 GC 周期极简化1. 并发标记 ── 找存活对象染色指针标记 M0/M1 2. 并发转移 ── 把存活对象从旧 Region 复制到新 Region 3. 并发重映射 ── 修复指针指向新地址染色位 R全部并发STW 只在每次阶段的极短同步点几微秒到几毫秒用于线程协调应用线程: ████│█████████████████│██████████████│█████████████ ↑ ↑ ↑ 同步点1 同步点2 同步点3 (~0.1ms) (~0.1ms) (~0.1ms)注意是亚毫秒级停顿——ZGC 的 STW 和堆大小几乎无关因为同步点只做线程协调不做对象扫描。这就是 ZGC 能在TB 级堆上仍保持亚毫秒停顿的根本原因。多重映射染色指针在高 4 位存元数据但硬件 MMU 不认这些位——直接解引用会段错误。ZGC 用多重映射Multi-Mapping解决同一物理内存映射到多个虚拟地址空间每个对应不同染色 虚拟地址视图 A (F0,M00,M10,R0) ──┐ 虚拟地址视图 B (F0,M01,M10,R0) ──┼──→ 同一物理内存 虚拟地址视图 C (F0,M00,M11,R0) ──┤ 虚拟地址视图 D (F0,M00,M10,R1) ──┘JVM 在启动时建立这些映射对象在不同染色状态下通过不同虚拟地址访问。这依赖操作系统的mmap支持是 ZGC 平台受限的原因之一早期只支持 Linux x64。ZGC 的演进JDK 版本状态特性JDK 11实验-XX:UnlockExperimentalVMOptions -XX:UseZGCJDK 13实验增加归还未使用内存JEP 351JDK 15正式移除实验标记可直接启用JDK 16优化并发栈扫描JEP 376JDK 21优化分代 ZGCJEP 439分代 ZGC 是重大改进——早期 ZGC 不分代所有对象一视同仁回收对小对象多的应用吞吐量偏低。JDK 21 的分代 ZGC 显著提升了吞吐量缩小了和 G1 的差距。Shenandoah 核心技术Shenandoah 由 Red Hat 开发2014 年立项JDK 12 进入主线实验JDK 15 转正。它的目标和 ZGC 一致——并发整理但实现路径完全不同用转发指针Brooks Pointer代替染色指针。转发指针每个对象头额外存一个指针指向自己的正式副本。对象移动时先复制到新地址再更新转发指针指向新地址——旧引用通过转发指针仍能找到对象。对象头布局Shenandoah ┌────────────────────────┐ │ 转发指针 (Brooks) ──────┼──→ 新地址移动后 ├────────────────────────┤ │ Mark Word │ ├────────────────────────┤ │ 实例数据 │ └────────────────────────┘对象未移动 引用 ──→ [转发指针: 自身] [Mark] [数据] ↑ 指向自己 对象移动后 引用 ──→ [转发指针: 新地址] [Mark] [数据(旧)] ← 旧引用 ↓ [转发指针: 自身] [Mark] [数据(新)] ← 新副本应用访问对象时读转发指针拿到真实地址——即使对象被移动旧引用通过转发指针仍能透明访问。这就是 Brooks Pointer 的转发机制。写屏障Shenandoah 用写屏障维护转发指针的正确性// 伪代码Shenandoah 写屏障voidfieldWrite(Objectsrc,Fieldf,ObjectnewValue){ObjectresolvedresolveForward(src);// 先解转发ObjectresolvedValresolveForward(newValue);resolved.fresolvedVal;}每次写都要先解析转发指针开销比 ZGC 的读屏障大写比读少但每次写都查。Shenandoah 历经多次优化JDK 13 的负载参考屏障、JDK 14 的弱记忆序优化逐步降低了写屏障开销。并发整理流程Shenandoah 的 GC 周期1. 初始标记 ── STW极短 2. 并发标记 ── 找存活对象 3. 最终标记 ── STW处理 SATB 队列 4. 并发清理 ── 回收无存活对象的 Region 5. 并发转移 ── 复制存活对象到新 Region更新转发指针 6. 初始更新引用 ── STW极短 7. 并发更新引用 ── 修复所有指向旧地址的引用 8. 最终更新引用 ── STW极短STW 点有三个初始标记、最终标记、更新引用每个都只做线程协调和堆大小无关。实测停顿通常在 1-10ms。ZGC vs Shenandoah 对比维度ZGCShenandoah整理机制染色指针 读屏障转发指针 写屏障状态存储指针高位染色位对象头转发指针堆开销指针高位无额外每对象多一个指针约 4-12% 堆读开销读屏障5-10%极低写开销极低写屏障较大但优化后可接受平台Linux/macOS x64/AArch64Linux x64/AArch64JDK 11实验不支持Red Hat 移植版JDK 15正式正式JDK 17正式正式分代JDK 21 起分代JDK 17 逐步分代厂商OracleRed Hat超大堆TB级优秀堆无关停顿良好开箱即用好JDK 15 直接启用好JDK 15 直接启用选型建议需求 → 推荐 ───────────────────────────────────────────── 超低延迟 大堆 32GB → ZGC 超低延迟 中小堆 → ZGC 或 Shenandoah 低延迟 高吞吐平衡 → G1仍是默认最优 批量/吞吐优先 → Parallel JDK 8 遗留 → CMS已废弃尽快升级两者差异不大时优先 ZGC原因Oracle 主推生态支持更广。JDK 21 分代 ZGC 性能提升显著。读屏障比写屏障对常见应用更友好读多写少。Shenandoah 的优势在于分代更早成熟JDK 17 就有较好分代支持和对 NUMA 的亲和性。在 Red Hat OpenJDK 发行版上Shenandoah 是一等公民。代码示例启用与观察/** * 演示 ZGC 行为 * 适用 JDK 15JDK 11-14 需加 -XX:UnlockExperimentalVMOptions * * 运行 * java -Xms4g -Xmx4g -XX:UseZGC * -XX:ZAllocationSpikeTolerance2 * -Xlog:gc*info -cp MyApp ZgcDemo */publicclassZgcDemo{staticfinalint_1MB1024*1024;publicstaticvoidmain(String[]args)throwsException{java.util.Listbyte[]listnewjava.util.ArrayList();for(inti0;i1000;i){list.add(newbyte[256*1024]);if(i%500)list.subList(0,list.size()/2).clear();Thread.sleep(1);}}}ZGC 日志[0.234s][info][gc,start] GC(0) Garbage Collection (Warmup) [0.234s][info][gc,task] GC(0) Using 4 workers [0.235s][info][gc,phases] GC(0) Pause Mark Start 0.023ms [0.235s][info][gc,phases] GC(0) Concurrent Mark 1.234ms [0.236s][info][gc,phases] GC(0) Pause Mark End 0.018ms [0.236s][info][gc,phases] GC(0) Concurrent Process Non-Strong 0.123ms [0.236s][info][gc,phases] GC(0) Concurrent Reset Relocation Set 0.045ms [0.237s][info][gc,phases] GC(0) Pause Relocate Start 0.019ms [0.237s][info][gc,phases] GC(0) Concurrent Relocate 0.234ms [0.237s][info][gc] GC(0) Garbage Collection (Warmup) 1500M-800M(4096M) 0.234ms关注几个点Pause Mark Start、Pause Mark End、Pause Relocate Start这是 STW 点每个都在0.0x 毫秒级别。Concurrent Mark、Concurrent Relocate并发阶段应用不停。总停顿0.234ms——这是在 4GB 堆上的表现即使堆涨到 100GB停顿仍在这个量级。Shenandoah 日志对比[0.234s][info][gc,start] GC(0) Pause Init Mark [0.235s][info][gc] GC(0) Pause Init Mark 0.234ms [0.235s][info][gc] GC(0) Concurrent marking [0.236s][info][gc,start] GC(0) Pause Final Mark [0.237s][info][gc] GC(0) Pause Final Mark 0.456ms [0.237s][info][gc] GC(0) Concurrent cleanup 1 [0.238s][info][gc] GC(0) Concurrent evacuation [0.240s][info][gc] GC(0) Pause Init Update Refs 0.023ms [0.240s][info][gc] GC(0) Concurrent update references [0.241s][info][gc] GC(0) Pause Final Update Refs 0.034msShenandoah 的 STW 点更多5 个但每个都极短。整体停顿在 1ms 量级和 ZGC 在同一档次。适用场景ZGC / Shenandoah 适合超大堆32GB-16TB停顿与堆无关TB 级堆也能亚毫秒停顿。极低延迟交易、风控、游戏服务器要求 P99 10ms。延迟敏感的微服务即使 GC 也不影响 SLA。大数据 / 内存数据库堆大且要交互式查询。暂不适合小堆 2GBG1 已足够ZGC/Shenandoah 的元数据开销不划算。吞吐量优先的批处理Parallel 仍是吞吐之王。JDK 8 遗留系统两者都不支持 JDK 8需升级。实践要点1. 启用方式# JDK 11-14实验java-XX:UnlockExperimentalVMOptions-XX:UseZGC-cpMyApp com.example.Main# JDK 15正式java-XX:UseZGC-cpMyApp com.example.Main# ShenandoahJDK 15java-XX:UseShenandoahGC-cpMyApp com.example.Main2. 验证是否生效java-XX:PrintFlagsFinal-version21|grep-EUseZGC|UseShenandoah确认UseZGC true才是真的启用了某些发行版如 Oracle JDK可能不包含 Shenandoah。3. JDK 21 分代 ZGC 是分水岭JDK 21 前 ZGC 不分代吞吐量比 G1 低 10-30%。JDK 21 后分代 ZGC显著提升吞吐量成为生产可用选择。升级到 JDK 21 是启用 ZGC 的好时机。# JDK 21 分代 ZGC默认启用java-XX:UseZGC-Xlog:gc*info-cpMyApp com.example.Main# 日志会显示 Generational ZGC4. 容器化部署注意ZGC/Shenandoah 需要正确的内存感知。在容器中java-XX:UseZGC\-XX:UseContainerSupport\-XX:MaxRAMPercentage75\-cpMyApp com.example.Main-XX:MaxRAMPercentage限制堆占容器内存的比例避免被 OOM Kill。5. 监控停顿用 JFRJava Flight Recorder记录 GC 事件java-XX:StartFlightRecordingduration60s,filenamerec.jfr\-XX:UseZGC-cpMyApp com.example.Main然后用 JMC 分析停顿分布确认是否达到 SLA。6. 不要过度调优ZGC/Shenandoah 设计为开箱即用默认参数已针对多数场景优化。生产中通常只需设-Xmx和-XX:MaxGCPauseMillisZGC 可选其余让 JVM 自适应。小结ZGC用染色指针指针高位存 GC 状态读屏障实现并发整理停顿与堆大小无关可达亚毫秒级适合 TB 级超大堆。Shenandoah用转发指针Brooks Pointer对象头指向正式副本写屏障实现并发整理停顿通常 1-10ms。两者核心突破把 G1 中 STW 的对象整理阶段并发化通过屏障机制让引用修复与应用线程并发进行。JDK 11实验JDK 15转正JDK 21分代 ZGC 是生产可用的里程碑。适用场景超低延迟 10ms、超大堆 32GB小堆和吞吐优先场景仍用 G1/Parallel。选型两者性能接近时优先 ZGCOracle 主推、分代成熟Red Hat 环境可用 Shenandoah。下一篇我们将进入实战——如何读懂 GC 日志、用工具分析、通过真实案例完成一次完整的 GC 调优。更多内容JVM调优实战