JVM 垃圾回收器选型进阶——G1、ZGC 与 Shenandoah 的性能场景对比一、背景JVM 垃圾回收器的选择直接影响应用的服务质量。随着 JDK 版本的演进从 JDK 9 默认使用 G1到 JDK 15 引入 ZGC 生产就绪再到 Shenandoah 在 JDK 12 后逐步成熟架构师在技术选型时面临的选择面越来越广但也越来越需要基于具体场景做决策。本文从生产环境实测数据出发对比 G1、ZGC 与 Shenandoah 在不同堆大小、不同延迟敏感度场景下的表现给出客观的选型建议。二、三种回收器核心原理对比graph TB subgraph G1 垃圾回收器 A1[堆内存分区 Region] -- A2[Young GCbr/STW 短暂] A2 -- A3[Mixed GCbr/并发标记 STW 回收] A3 -- A4[Full GCbr/退化为单线程br/STW 时间不可控] end subgraph ZGC 垃圾回收器 B1[染色指针 Colored Pointers] -- B2[并发标记br/读屏障] B2 -- B3[并发转移br/自愈重映射] B3 -- B4[并发重映射] end subgraph Shenandoah 垃圾回收器 C1[转发指针 Brooks Pointers] -- C2[并发标记] C2 -- C3[并发整理br/Brooks 指针更新] C3 -- C4[并发更新引用] end style A4 fill:#ffcdd2 style B3 fill:#c8e6c9 style C3 fill:#c8e6c9关键差异对比特性G1ZGCShenandoah并发阶段标记阶段并发回收不并发全阶段并发标记、转移、重映射全阶段并发核心技术Region 分区 记忆集染色指针 读屏障Brooks 转发指针最大 STW 时间10ms目标Full GC 可达秒级1ms亚毫秒10ms堆大小支持4GB~64GB 表现较优8MB~16TB2GB~128GB吞吐量开销约 5-10%约 10-15%读屏障开销约 10-15%JDK 最低版本JDK 7u4JDK 11实验/JDK 15生产JDK 12三、生产验证方案3.1 测试环境与配置/** * 测试用的 REST 接口模拟实际业务负载 * 场景 A频繁分配大对象模拟订单处理链路 * 场景 B大量短生命周期对象模拟 API 网关 */ RestController RequestMapping(/benchmark) public class GcBenchmarkController { private static final Listbyte[] OBJECT_STORE Collections.synchronizedList(new ArrayList()); private static final Random RANDOM new Random(); GetMapping(/scenario-a) public ResponseEntityString scenarioA() { try { // 场景 A分配 1MB 随机大小对象部分存活 int size 512 * 1024 RANDOM.nextInt(512 * 1024); // 512KB~1MB byte[] data new byte[size]; if (RANDOM.nextDouble() 0.3) { // 为什么保留 30% 对象模拟实际业务中缓存/会话等长生命周期对象 OBJECT_STORE.add(data); } // 定期清理防止 OOM if (OBJECT_STORE.size() 1000) { OBJECT_STORE.subList(0, 500).clear(); } return ResponseEntity.ok({\result\:\success\}); } catch (OutOfMemoryError e) { // 为什么要捕获 OutOfMemoryError // 1. 在生产环境中不能让单个 OOM 导致整个 JVM 崩溃 // 2. 及时清理引用给 GC 回收的机会 OBJECT_STORE.clear(); System.gc(); // 建议 JVM 执行 Full GC return ResponseEntity.status(500) .body({\error\:\memory exhausted, recovered\}); } } }3.2 GC 日志分析命令# G1 GC 启动参数 -XX:UseG1GC -XX:MaxGCPauseMillis100 -Xms8g -Xmx8g -Xlog:gc*:file/tmp/gc-g1.log:time,level,tags # ZGC 启动参数 -XX:UseZGC -Xms8g -Xmx8g -Xlog:gc*:file/tmp/gc-zgc.log:time,level,tags # 分析脚本统计 STW 时间分布 # 为什么关注 P99 而非平均值GC 导致的响应毛刺才是用户感知到的延迟 jstat -gcutil pid 1000 | awk {print $1, $10}四、性能对比数据测试环境16 Core/32GBJDK 21堆大小 8GB并发 200 QPS持续压测 10 分钟。指标G1 (XX:MaxGCPauseMillis100)ZGCShenandoah平均响应时间12ms14ms15msP99 响应时间85ms18ms32msP99.9 响应时间410msFull GC 引发21ms48ms最大 STW 时间2.8sFull GC0.8ms8.2ms吞吐量1950 ops/s1820 ops/s1780 ops/sCPU 使用率65%72%70%graph LR subgraph STW 时间对比 (ms) direction TB D1[G1: 平均 15msbr/最大 2800ms] D2[ZGC: 平均 0.3msbr/最大 0.8ms] D3[Shenandoah: 平均 3msbr/最大 8.2ms] end style D1 fill:#ffcdd2 style D2 fill:#c8e6c9 style D3 fill:#fff9c4五、边界分析与 Trade-offs5.1 G1 的适用边界G1 在堆内存 4GB~32GB 区间内表现稳定但存在明确的性能拐点。当堆内存超过 32GB 时Region 数量急剧增加记忆集Remembered Set的维护成本呈平方级增长。在生产环境中观察到64GB 堆下G1 的 Mixed GC 阶段可能出现 3-5 秒的 STW这是因为 G1 需要扫描所有 Region 的记忆集来确定回收收益。此外G1 的-XX:MaxGCPauseMillis参数只是一个目标而非保证。当存活对象比例过高时G1 无法在目标时间内完成回收会逐步放弃更多的 Region 回收最终导致 Full GC。因此对于堆中存活对象长期超过 60% 的应用如大型缓存服务G1 不是最优选择。5.2 ZGC 的成本分析ZGC 的亚毫秒级 STW 并非免费午餐。其核心代价来自两方面读屏障开销ZGC 使用加载屏障Load Barrier在每次从堆中读取对象引用时执行额外逻辑染色指针检查与自愈。在对象访问频繁的场景下如序列化/反序列化、反射调用读屏障会导致 10-15% 的吞吐量损失。实测数据显示在对象分配速率 1GB/s 的场景中ZGC 的吞吐量比 G1 低约 12%。内存开销ZGC 的多映射Multi-Mapping技术需要预留更多的虚拟地址空间。在多租户容器环境如 K8s 限制内存配额中需要额外关注进程的 VIRT 内存占用避免因虚拟内存超限被 OOM Killer 终止。5.3 Shenandoah 的定位Shenandoah 的设计目标与 ZGC 类似但实现路径不同Brooks 指针 vs 染色指针。在 JDK 17 的环境中Shenandoah 的 P99 延迟表现与 ZGC 接近但最大 STW 时间略高约 5-10ms。其优势在于对堆大小的适应性更强实测在 128GB 堆上仍能稳定工作且 CPU 开销略低于 ZGC约 8-12% vs 10-15%。但需要注意的是Shenandoah 在 Red Hat 发行版中支持更完善OpenJDK 官方构建的兼容性需要仔细验证。在生产环境使用前必须在目标 JDK 版本上完成完整的回归测试。六、总结与选型建议选型决策树堆内存 4GB→ 保持 G1ZGC 和 Shenandoah 的并发开销在小堆上收益不明显堆内存 4GB~32GB对 P99 延迟敏感50ms 要求→ 优先 ZGC亚毫秒级 STW 是最大优势堆内存 4GB~32GB对吞吐量敏感延迟可接受 100ms→ 保持 G1吞吐量损失最小的选择堆内存 32GB→ 必须选 ZGCG1 在大堆上 Full GC 风险显著增大JDK 版本 17→ G1最兼容Shenandoah 要求 JDK 12ZGC 推荐 JDK 17关键认知GC 选型没有最优只有最合适。ZGC 在延迟上碾压 G1但付出了约 7% 的吞吐量代价和更高的 CPU 开销。如果你的服务对延迟敏感如交易系统、网关这 7% 的代价值得支付如果你的服务是离线批处理G1 的吞吐量优势更有价值。