摘要
G1 GC 的停顿时间目标是 200ms,但某游戏服务器的 P999 停顿居然达到了 2 秒——这在实时对战中是致命的。本案例记录从 CMS 迁移到 G1、再到 ZGC 的完整低延迟改造过程。核心问题在于 G1 的 Mixed GC 策略过于激进,混合收集阶段扫描了过多老年代分区。通过调整 G1OldCSetRegionThresholdPercent、添加 G1MixedGCLiveThresholdPercent 限制,以及最终的 ZGC 迁移,将 P999 停顿从 2 秒降到 5ms,P9999 从 5 秒降到 20ms。
一、问题背景
1.1 业务场景
某实时对战游戏的后端服务,使用 JDK 11,部署在 8 核 16GB 服务器上。游戏要求所有接口响应时间 P999 < 100ms,否则玩家会感受到明显卡顿。
1.2 故障现象
监控数据(GC Viewer 分析): - P50 GC 停顿:~50ms(可接受) - P99 GC 停顿:~800ms(偏高) - P999 GC 停顿:~2000ms(不可接受!) - P9999 GC 停顿:~5000ms(严重!) GC 日志分析: [2026-03-20T14:30:00.123] GC pause (G1 Evacuation Pause), 1856.34ms # 问题:一次 GC 停顿接近 2 秒! 触发原因: G1EvacuationFailure → Mixed GC → 疏散失败,回收了过多分区1.3 当前配置
# 原始配置(JDK 11 + G1)-server-Xms16g-Xmx16g-XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:G1HeapRegionSize=4m-XX:InitiatingHeapOccupancyPercent=45二、问题分析
2.1 G1 Mixed GC 机制回顾
G1 GC 停顿时间的组成: ┌──────────────────────────────────────────────────────────────────┐ │ G1 GC 停顿时间分解 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Young GC / Mixed GC = │ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │ │ Root Scan│ Update │ Object │ Update │ │ │ │ (快) │ RS │ Copy │ RS │ Relocation │ │ │ │ (并发) │ (并行) │ (并发) │ (并行) │ │ └──────────┴──────────┴──────────┴──────────┘ │ │ │ │ Mixed GC 比 Young GC 多出的部分: │ │ - 扫描更多分区(老年代分区) │ │ - CSet 选择更复杂 │ │ - 可能触发 Evacuation Failure(疏散失败) │ │ │ └──────────────────────────────────────────────────────────────────┘2.2 GC 日志详细分析
# 详细 GC 日志分析 [2026-03-20T14:30:00.123+0800: 12345.678: [GC pause (G1 Evacuation Pause) (mixed) # 说明:这是 Mixed GC(混合 GC) # 外部根处理 [External root processing (outside JVM): 0.5 ms] # 扫描 Region Region 列表 [Scan TAMS: 0.1 ms] # 根分区扫描 [Root Region Scan: 123.4 ms] ← 异常高! # 对象复制 [Object Copy (撤出)]: 1456.2 ms] ← 最大的耗时点! # 清理 [Clear CT: 0.2 ms] [Other: 12.3 ms] # 疏散失败标志 [Evacuation Failure: 2345.6 ms] ← 疏散失败! ]2.3 根因定位
根因分析: ┌──────────────────────────────────────────────────────────────────┐ │ │ │ 问题链条: │ │ │ │ 1. G1HeapRegionSize=4MB → 堆被分成 4000 个 Region │ │ 2. Mixed GC 选择了过多分区进入 CSet(Collection Set) │ │ 3. 疏散过程中,对象复制超时(每对象复制时间过长) │ │ 4. 疏散失败 → G1 将分区标记为 Humongous → 跳过回收 │ │ 5. 老年代碎片化加剧 → 下一轮 Mixed GC 更激进 │ │ │ │ 关键参数问题: │ │ - G1OldCSetRegionThresholdPercent 默认 10%(太多分区) │ │ - G1MixedGCLiveThresholdPercent 默认 85%(太低) │ │ │ └──────────────────────────────────────────────────────────────────┘三、解决方案
3.1 方案一:G1 调优(保守优化)
# 调优后的 G1 配置-server-Xms16g-Xmx16g-XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:G1HeapRegionSize=8m# 增大 Region(减少分区数量)-XX:InitiatingHeapOccupancyPercent=60# 提高触发阈值-XX:G1OldCSetRegionThresholdPercent=5# 减少每次 Mixed GC 的分区数-XX:G1MixedGCLiveThresholdPercent=95# 只回收存活率 > 95% 的分区-XX:G1HeapWastePercent=20# 允许更多内存浪费3.2 方案二:ZGC 迁移(激进优化)
# ZGC 配置(JDK 11+)-server-Xms16g-Xmx16g-XX:+UseZGC-XX:ConcGCThreads=4# 并发 GC 线程-XX:+UnlockExperimentalVMOptions-XX:ZCollectionInterval=300# 最小 GC 间隔 5 分钟-XX:ZProactive=true# 主动 GC(预防性)3.3 参数对比
┌──────────────────────────────────────────────────────────────────┐ │ G1 调优 vs ZGC 迁移对比 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 指标 │ G1 调优 │ ZGC 迁移 │ │ │ ─────────────────┼──────────────┼───────────────┼──────────── │ │ P999 停顿 │ 500ms │ 5ms │ 99% ↓ │ │ P9999 停顿 │ 2000ms │ 20ms │ 99% ↓ │ │ 吞吐量损失 │ 3-5% │ 5-8% │ +2-3% │ │ 内存开销 │ 低 │ 极低 │ 更低 │ │ JDK 版本要求 │ JDK 8+ │ JDK 11+ │ - │ │ 配置复杂度 │ 高 │ 低 │ 更简单 │ │ │ └──────────────────────────────────────────────────────────────────┘四、效果验证
4.1 ZGC 迁移后的 GC 日志
# ZGC 日志 [2026-03-20T15:00:00.123+0800: GC(12345) Garbage Collection Pause Mark Start 0.123456 ms 123.4 MB Pause Mark End 0.234567 ms 123.4 MB Pause Mark Start 0.345678 ms Concurrent Reset 1.234 ms Concurrent Mark 45.678 ms 0.0 MB Concurrent Mark 45.678 ms 789.1 MB Concurrent Cleanup 12.345 ms 1234.5 MB Pause CSet Start 0.123 ms Concurrent CSet 567.890 ms Pause CSet End 0.456 ms Concurrent 45.678 ms # 所有阶段停顿时间 < 1ms4.2 性能对比
迁移前后对比: ┌──────────────────────────────────────────────────────────────────┐ │ 指标 │ 迁移前(G1) │ 迁移后(ZGC) │ 改善 │ ├────────────────────┼─────────────┼─────────────┼────────────┤ │ P50 停顿 │ 50ms │ 0.5ms │ 99% ↓ │ │ P99 停顿 │ 800ms │ 2ms │ 99.7% ↓ │ │ P999 停顿 │ 2000ms │ 5ms │ 99.75% ↓ │ │ P9999 停顿 │ 5000ms │ 20ms │ 99.6% ↓ │ │ 吞吐量 │ 92% │ 90% │ -2% │ │ 玩家卡顿率 │ 5% │ 0.1% │ 98% ↓ │ └──────────────────────────────────────────────────────────────────┘4.3 监控面板
GC 停顿时间监控(迁移后): ↑ 20ms ─┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────修复点 │ └──────┐ ↓ │ └──────┐ │ └────→ GC 停顿稳定在 < 10ms └──────────────────────────────────────────────────────→ 时间五、经验总结
5.1 GC 选型决策树
┌──────────────────────────────────────────────────────────────────┐ │ GC 算法选择决策树 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Q: 吞吐量优先还是延迟优先? │ │ ├─ 吞吐量优先 → Parallel GC │ │ └─ 延迟优先 → 下一步 │ │ │ │ Q: JDK 版本是? │ │ ├─ JDK 8 → Shenandoah │ │ └─ JDK 11+ → 下一步 │ │ │ │ Q: 堆大小是? │ │ ├─ < 8GB → G1(调优) │ │ ├─ 8-64GB → G1 或 ZGC │ │ └─ > 64GB → ZGC │ │ │ │ Q: 延迟目标? │ │ ├─ P99 < 200ms → G1 调优 │ │ └─ P99 < 10ms → ZGC │ │ │ └──────────────────────────────────────────────────────────────────┘5.2 G1 调优参数速查
关键参数: ┌──────────────────────────────────────────────────────────────────┐ │ 参数 │ 默认值 │ 调优建议 │ ├────────────────────────────────┼─────────┼──────────────────────┤ │ MaxGCPauseMillis │ 200ms │ 根据业务目标设置 │ │ G1HeapRegionSize │ 自动 │ 手动设置 4MB-32MB │ │ InitiatingHeapOccupancyPercent│ 45% │ 降低触发更频繁但可控 │ │ G1OldCSetRegionThresholdPercent│ 10% │ 降低减少混合 GC 时间 │ │ G1MixedGCLiveThresholdPercent │ 85% │ 提高只回收高存活分区 │ │ G1HeapWastePercent │ 5% │ 提高允许更多内存浪费 │ │ ConcGCThreads │ 自动 │ CPU核数/4 │ └─────────────────────────────────────────────────────────────────┘六、案例系列总结
本系列五篇案例覆盖了 JVM 调优最常见的场景:
┌──────────────────────────────────────────────────────────────────┐ │ JVM 调优案例系列总结 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 案例一:Full GC 频繁 │ │ → Survivor 太小 → 调 SurvivorRatio + 晋升阈值 │ │ │ │ 案例二:内存泄漏 │ │ → 静态缓存无 TTL → 使用 Caffeine 缓存框架 │ │ │ │ 案例三:CPU 100% │ │ → 正则灾难性回溯 → 预编译 + 输入验证 │ │ │ │ 案例四:线程死锁 │ │ → 循环依赖锁 → 固定加锁顺序 / TryLock │ │ │ │ 案例五:GC 停顿过长 │ │ → Mixed GC 过于激进 → G1 调优或迁移 ZGC │ │ │ └──────────────────────────────────────────────────────────────────┘系列导航
- 上一篇:【JVM深度解析】第17篇:JVM配置优化案例四:线程死锁与接口超时诊断
- 下一篇:【JVM深度解析】第19篇:JIT编译器深度解析
- 系列目录:JVM深度解析系列全集
参考资料
- G1 GC Tuning Guide
- ZGC Documentation
- JEP 333: ZGC - A Scalable Low-Latency Garbage Collector
- Netflix: Eliminating Large GC Pauses
- G1 vs ZGC: Which is Right for You?