Java JVM 调优实战从 G1 到 ZGC让 AI 推理服务的 GC 停顿降到 1ms前言线上有个 Java 写的 AI 推理网关负责把用户请求转发到下游模型服务顺便做些前处理和后处理。问题是每隔几十秒P99 延迟会突然从 20ms 飙到 300ms。排查后发现是 G1 GC 的 Mixed GC 阶段造成的 STW 停顿。换成 ZGC 之后GC 停顿从 150ms 降到 0.5ms。这篇记录完整的调优过程。一、问题诊断1.1 延迟毛刺现象# GC 日志中的关键信息G1 收集器 [GC pause (G1 Evacuation Pause) (mixed), 0.1523842 secs] [Parallel Time: 148.2 ms, GC Workers: 8] [Eden: 512M-0B Survivors: 64M-64M Heap: 2048M-1536M]每次 Mixed GC 停顿 150ms。对于 AI 推理网关来说这是不可接受的。1.2 为什么 AI 推理服务对 GC 敏感graph LR A[用户请求] -- B[Java 推理网关] B -- C[特征提取\n大量临时对象] C -- D[模型推理调用] D -- E[结果后处理\n JSON 序列化] E -- F[响应返回] C --|触发 GC| G[STW 停顿 150ms ❌] style G fill:#ef4444,color:#fffAI 推理场景的特点大量临时对象特征提取时创建大量 float 数组和中间对象存活时间短请求处理完就丢弃Young GC 频繁堆内存大为了缓存模型 metadata堆设到 4G二、G1 调优治标2.1 基础参数# G1 收集器基础配置 java -Xms4g -Xmx4g \ -XX:UseG1GC \ -XX:MaxGCPauseMillis50 \ -XX:G1HeapRegionSize16m \ -XX:InitiatingHeapOccupancyPercent45 \ -XX:G1ReservePercent15 \ -jar 推理网关.jar2.2 核心参数解释public class G1调优参数说明 { // MaxGCPauseMillis目标停顿时间 // 设太低(10ms) - GC 频率暴增吞吐量下降 // 设太高(200ms) - 停顿太长延迟毛刺明显 // 经验值50ms 是一个平衡点 static final int 目标停顿 50; // G1HeapRegionSizeRegion 大小 // 堆 4G 时设 16m - 256 个 Region // 太小(1m) - Region 数量太多管理开销大 // 太大(32m) - 内存碎片化严重 static final int REGION大小 16; // MB // InitiatingHeapOccupancyPercent触发并发标记的阈值 // 设低(30%) - GC 更频繁但每次回收量少停顿短 // 设高(70%) - GC 少但每次回收量大停顿长 static final int 并发标记阈值 45; // % }2.3 G1 调优效果指标调优前调优后Mixed GC 停顿150ms55msYoung GC 停顿15ms12msGC 频率每 30s 一次每 20s 一次P99 延迟300ms80ms从 300ms 降到 80ms。好了一些但还不够。目标是 10ms 以下。三、切换 ZGC治本3.1 ZGC 是什么一句话几乎零停顿的垃圾收集器。GC 停顿时间与堆大小无关不管你堆是 4G 还是 16TB停顿都控制在亚毫秒级。graph TD subgraph G1 A[标记阶段\nSTW 10-50ms] -- B[清理阶段\nSTW 50-200ms] end subgraph ZGC C[并发标记\nSTW 1ms] -- D[并发重定位\nSTW 1ms] end style B fill:#ef4444,color:#fff style D fill:#10b981,color:#fff3.2 ZGC 启用配置# ZGC 配置JDK 21 java -Xms4g -Xmx4g \ -XX:UseZGC \ -XX:ZGenerational \ -XX:SoftMaxHeapSize3g \ -XX:ConcGCThreads4 \ -Xlog:gc*:filegc.log:time,uptime,level,tags \ -jar 推理网关.jar3.3 关键参数public class ZGC参数详解 { // ZGenerational开启分代 ZGCJDK 21 新特性 // 分代后 Young GC 更高效推荐开启 // JDK 21 之前没有这个选项 // SoftMaxHeapSize软上限 // ZGC 会尽量把堆控制在这个值以下 // 设成物理内存的 75% 是个好选择 // 留 25% 给 ZGC 的染色指针元数据 // ConcGCThreads并发 GC 线程数 // 默认值 CPU 核数 / 4 // AI 推理场景 CPU 密集GC 线程不要抢太多资源 // 8 核机器设 2-4 个就够 }3.4 ZGC 效果# ZGC 的 GC 日志 [gc,start] GC(36) Garbage Collection (Proactive) [gc ] GC(36) Pause Mark Start 0.018ms [gc ] GC(36) Concurrent Mark 12.453ms [gc ] GC(36) Pause Mark End 0.012ms [gc ] GC(36) Concurrent Process Non-Strong References 1.234ms [gc ] GC(36) Concurrent Relocate 8.765ms [gc ] GC(36) Pause Relocate Start 0.008ms看到了吗所有 Pause 阶段加起来不到0.04ms。3.5 最终对比指标G1默认G1调优后ZGC最大 GC 停顿150ms55ms0.5msP99 延迟300ms80ms22ms吞吐量4200 QPS4000 QPS3900 QPS堆内存开销4G4G4.6G15%⚠️ ZGC 的代价吞吐量略降 5%内存多占 15%。但对于延迟敏感的 AI 推理服务这个交换太值了。四、生产级监控代码import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.util.List; public class GC监控器 { /** * 打印当前 GC 统计信息 * 可以接入 Prometheus 做持续监控 */ public static void 打印GC统计() { ListGarbageCollectorMXBean gcBeans ManagementFactory.getGarbageCollectorMXBeans(); for (GarbageCollectorMXBean gc : gcBeans) { String 收集器名称 gc.getName(); long 收集次数 gc.getCollectionCount(); long 总耗时毫秒 gc.getCollectionTime(); double 平均耗时 收集次数 0 ? (double) 总耗时毫秒 / 收集次数 : 0; System.out.printf( 收集器: %s | 次数: %d | 总耗时: %dms | 平均: %.2fms%n, 收集器名称, 收集次数, 总耗时毫秒, 平均耗时 ); } } /** * 获取当前堆内存使用情况 */ public static void 打印内存使用() { Runtime runtime Runtime.getRuntime(); long 最大内存 runtime.maxMemory() / 1024 / 1024; long 已分配 runtime.totalMemory() / 1024 / 1024; long 已使用 (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024; System.out.printf( 堆内存 - 最大: %dMB | 已分配: %dMB | 已使用: %dMB | 使用率: %.1f%%%n, 最大内存, 已分配, 已使用, (double) 已使用 / 最大内存 * 100 ); } public static void main(String[] args) { // 每 5 秒打印一次 while (true) { 打印GC统计(); 打印内存使用(); System.out.println(---); try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }五、避坑指南5.1 ZGC 的版本要求⚠️ 不同 JDK 版本的 ZGC 能力差异很大JDK 版本ZGC 状态JDK 11实验性不建议生产使用JDK 15正式发布但非分代JDK 17稳定可用推荐的 LTS 版本JDK 21分代 ZGC最佳选择5.2 容器环境注意事项# Docker 容器中必须正确设置内存限制 # 否则 JVM 可能看到宿主机的全部内存 docker run -m 6g \ -e JAVA_OPTS-Xms4g -Xmx4g -XX:UseZGC \ 推理网关:latest # JDK 10 默认识别容器内存限制 # 但建议显式设置 -Xmx 确保可控5.3 不适合 ZGC 的场景堆内存 256MB 的小应用ZGC 本身开销不划算纯吞吐型的离线批处理G1 的吞吐量更高JDK 版本低于 17ZGC 不够稳定六、总结两条路线G1 调优调MaxGCPauseMillisG1HeapRegionSizeIHOP能压到 50ms 左右切 ZGC直接把停顿干到亚毫秒级代价是 5% 吞吐量和 15% 额外内存对于 AI 推理服务这种延迟敏感场景别犹豫直接上 ZGC。数据已经说明一切。