G1 GC 的 MaxGCPauseMillis 设置 100 ms 却经常 200 ms,如何调优

解读

面试官抛出该问题,核心想验证三件事:

  1. 候选人是否真正理解 MaxGCPauseMillis 只是“期望值”而非“硬阈值”;
  2. 能否把“GC 日志—内存模型—业务特征”三条线串起来定位根因;
  3. 在国产服务器、国产 JDK(阿里 Dragonwell、腾讯 Kona、华为 Bisheng 等)以及常见云环境(阿里云 ECS、腾讯云 CVM、华为云 HECS)下,是否具备可落地的调参套路和容量评估经验。

一句话:不是简单改参数,而是让面试官看到你“用数据说话、让 pause 可控”的完整思路。

知识点

  1. G1 的收集阶段:Young GC、并发标记、混合收集、Full GC;Pause 主要发生在 Young GC 和混合收集的 Evacuate 阶段。
  2. MaxGCPauseMillis 本质:收集器根据历史停顿动态调整“最大年轻代大小”和“每次混合收集的 old region 数”,但只保证“尽力”,不保证“一定”。
  3. 触发超预期的常见根因:
    • 年轻代太大或太小:太大单次复制耗时高,太小触发频繁;
    • 对象晋升速率 > 预测模型,导致混合收集时 old region 堆积;
    • 大对象(Humongous Object)直接进 old 区,造成一次性回收压力;
    • 业务突发浪涌,分配速率瞬间翻倍,GC 预测模型滞后;
    • 国产 ARM 服务器 NUMA 拓扑下,内存带宽瓶颈放大复制时间;
    • 宿主机超卖或内存压缩,导致 GC 线程被抢占。
  4. 必备日志项:-Xlog:gc*,safepoint,ergo*=debug;重点看“Pause Young”“Pause Mixed”与“Desired pause time”对比。
  5. 调优工具链:gcplot、gceasy、perf、FlameGraph、阿里 arthas 的 heapdump + profiler,以及腾讯云 tsar-gc 插件。
  6. 国内生产规范:金融类 SLA 一般 99.9% 请求 < 100 ms,因此 GC 平均 100 ms、P99 150 ms 是常见红线;政务云环境要求 Full GC 为零。

答案

回答采用“四步闭环”法:量化现状 → 根因定位 → 参数干预 → 灰度验证。

第一步:量化现状
上线 -Xlog:gc*=info:file=gc.log:time,level,tags:filecount=5,filesize=50M,压测 30 分钟,采集“Pause Young”“Pause Mixed”耗时。用 gcplot 算出 P50、P99、P999 及对应吞吐量。假设得出 P99 Young Pause 210 ms,Mixed Pause 180 ms,已超目标。

第二步:根因定位

  1. 看“Ergonomics”日志,Desired pause time=100 ms,但年轻代实际标记 600 region,预测模型算出 95 ms,实际却 210 ms,说明“复制阶段”耗时异常。
  2. 检查大对象:日志出现“Humongous object 8 M > region size 4 M”,一次性分配 200 个,直接占 50 个 old region,导致后续 Mixed GC 需要回收更多 region。
  3. 结合压测脚本,发现 5 分钟一次“批量导入”接口,QPS 瞬间从 500 升到 3000,对象分配速率由 200 MB/s 涨到 1.2 GB/s,GC 预测模型来不及收缩年轻代。
  4. 用 perf 采样,发现 GC 线程 30% 时间在 memcpy,宿主机 CPU steal 10%,判断云环境超卖。

第三步:参数干预

  1. 把 region 大小从 4 M 调到 8 M,减少 Humongous 对象数量:-XX:G1HeapRegionSize=8m。
  2. 让年轻代上限更“紧”,迫使 G1 提前触发:-XX:MaxNewSize=1g(原 2 g),-XX:G1MaxNewSizePercent=20(原默认 60)。
  3. 提高并发线程数,降低 Evacuate 阶段 STW:-XX:ParallelGCThreads=12,-XX:ConcGCThreads=4(宿主机 16 vCPU)。
  4. 提前启动混合收集,避免 old region 堆积:-XX:InitiatingHeapOccupancyPercent=30(原 45)。
  5. 对导入接口做限流,削平浪涌:Sentinel QPS 阈值 1500,拒绝后异步削峰。
  6. 与云厂商沟通,换到独享型实例,CPU steal 降到 1% 以内。

第四步:灰度验证
重新 1.5 倍峰值压测 2 h,结果 P99 Young Pause 92 ms,Mixed Pause 88 ms,P999 118 ms,Full GC 0 次,CPU 利用率 65%,满足 SLA。上线灰度 20% 节点,监控 cat/arms 无接口超时,最终全量发布。

如果仍偶发 120 ms,可再细调:

  • 把 -XX:+UseStringDeduplication 开启,减少字符串副本;
  • 或者将 MaxGCPauseMillis 降到 80 ms,让模型更激进,但需接受吞吐量下降 3% 以内,用压测权衡。

拓展思考

  1. 如果业务是国产 ARM+Dragonwell11,JDK 对 NUMA 感知默认关闭,可开启 -XX:+UseNUMA 并绑定容器到同一 NUMA node,复制阶段耗时可再降 10%。
  2. 在 Serverless 场景(阿里云函数计算、腾讯云 SCF),冷启动后堆内存快速涨到 4 G,但存活对象极少,可调大 -XX:G1NewSizePercent=40,让几乎整个堆做年轻代,避免并发标记,实现“0 混合 GC”。
  3. 若未来升级到 ZGC,可把 MaxGCPauseMillis 设到 10 ms 级别,但 ZGC 的 15% 额外内存开销在 4 G 以下容器不划算,需要先做容量成本评估。