
停顿预测模型不是定时器,它依赖历史 GC 数据做回归估算
G1 的 MaxGCPauseMillis 不是硬性超时开关,而是触发“预测-决策-执行”闭环的起点。它本身不控制频率,真正决定 Young GC 或 Mixed GC 触发节奏的是:Eden 区填满速度、并发标记进度、以及模型对每个 Region 回收耗时的历史拟合结果。
模型内部维护一个“Region 回收成本表”,每轮 GC 后会更新每个 Region 类型(如 Eden 中小对象密集区、Humongous 邻近区)的平均扫描+复制时间。下一次回收前,G1 用这些数据预估:回收 N 个 Region 需要多少毫秒 —— 若预估值接近 MaxGCPauseMillis,就停止继续选 Region;若远低于,则多收几个,提升吞吐。
- 预测失效常见于冷启动后前 3~5 次 GC:模型没足够样本,容易低估大对象 Region 的复制开销
- 频繁分配大对象(>½
G1HeapRegionSize)会导致 Humongous 区激增,但模型默认不把 Humongous Region 纳入预测范围,实际停顿可能突增 - 日志中出现大量
[GC pause (G1 Evacuation Pause) (young)但单次耗时稳定在 80ms,说明模型已收敛;若耗时从 120ms 跳到 250ms,大概率是某批 Region 的存活对象比例突升,模型还没来得及修正
看懂 GC 日志里的 Predicted 和 Actual 才能反推模型是否可信
启用 -Xlog:gc+ergo=debug 后,每次 Young GC 或 Mixed GC 日志末尾会出现类似这样的行:
[12.456s][debug][gc,ergo] GC(42) predicted pause time: 142.3ms, target: 150.0ms, actual: 138.7ms
关键不是 actual 是否小于 target,而是 predicted 和 actual 的偏差是否持续扩大:
- predicted 20ms:模型低估了某些 Region 的复制压力,常见于 Survivor 区对象年龄突增或跨代引用剧增
- predicted > actual 且差值 >30ms:模型过于保守,可能因上次 GC 发生了
Concurrent Mode Failure导致它拉高了安全系数 - mixed GC 中 predicted 值反复在 80~180ms 波动:说明老年代 Region 的存活率不稳定,需检查是否有缓存未设上限或批量任务集中晋升
调整回收频率不能只改 MaxGCPauseMillis,得配合年轻代弹性边界
单纯把 MaxGCPauseMillis 从 200 改成 100,G1 并不会让 GC 更“勤快”,反而可能因每次回收 Region 数减少,导致 Eden 更快填满 —— 表现为 Young GC 频率上升,但吞吐下降。真正可控的杠杆是:
-
-XX:G1NewSizePercent=15和-XX:G1MaxNewSizePercent=45:扩大年轻代浮动区间,让 G1 在分配压力大时自动多划几个 Eden Region,延缓触发 Young GC -
-XX:G1MixedGCCountTarget=8:混合回收拆成更多小批次,每批更轻,但总次数增加 —— 这比强行压低MaxGCPauseMillis更利于维持停顿稳定性 - 禁用
-XX:-G1UseAdaptiveIHOP+ 固定-XX:InitiatingHeapOccupancyPercent=35:当业务内存增长模式极规律时,手动锁定并发标记启动点,避免自适应 IHOP 在流量波峰时误判
Region 大小错配会让预测模型彻底失准
G1HeapRegionSize 决定了每个 Region 的物理容量,也隐式影响预测模型的粒度。如果应用 90% 对象大小在 64KB~256KB 之间,而 G1HeapRegionSize=4m,那一个 Region 可能塞进上百个对象 —— 模型按 Region 整体估算回收成本,但实际只有其中 20% 对象存活,复制开销被严重高估。
此时模型会倾向于少收 Region,导致 Eden 区更快溢出,Young GC 频率被动抬高。正确做法是让 Region 大小贴近主流对象尺寸的 2~4 倍:
- 小对象密集(如微服务 DTO):
-XX:G1HeapRegionSize=1m - 中等对象为主(如缓存 value):
-XX:G1HeapRegionSize=2m或4m - 含大量 1MB+ 缓存块:
-XX:G1HeapRegionSize=8m,并监控 Humongous 分配日志是否频繁出现humongous allocation
Region 大小一旦设定,整个堆划分即固化,无法运行时调整 —— 这是最容易被忽略的底层约束。











