线上 Full GC 频繁,如何排查解决?
结论先行:Full GC 频繁先分两类:回收不动(老年代里全是活对象,泄漏或缓存过大)和回收太勤(对象晋升过快、元空间反复扩容、被显式触发)。判断只需一条:Full GC 之后老年代使用率是否明显下降。不降 → 查持有者;降了 → 查分配速率与晋升策略。然后再决定是改代码还是调参数。
一、症状与根因对照
| 症状 | 可能的根因 | 验证手段 |
|---|---|---|
| Full GC 后老年代仍在 90% 以上 | 内存泄漏、超大本地缓存 | heap dump + Dominator Tree |
| Full GC 后回落到 30% 但很快又满 | 分配速率过高、缓存无上限 | jstat -gcutil 看 Eden 增速 |
| 元空间持续增长、Full GC 伴随 Metaspace 扩容 | 动态类过多、热部署、CGLIB 膨胀 | jcmd GC.class_stats、-Xlog:class+load |
日志出现 System.gc() 调用 | 业务代码、RMI、DirectByteBuffer 清理 | -XX:+ExplicitGCInvokesConcurrent 或 Arthas stack |
日志出现 concurrent mode failure | CMS 并发收集追不上 | 提前阈值、换 G1 |
日志出现 to-space exhausted / evacuation failure | G1 没有空闲 Region 可搬 | 调 G1ReservePercent、查 Humongous |
二、排查路径
① 确认事实:多久一次、每次多久、回收效果
jstat -gcutil <pid> 1000 60 → 看 O、FGC、FGCT、YGC
grep "Pause Full" gc.log → 看频率与停顿
│
② 判断性质:Full GC 后老年代是否回落?
├─ 不回落 → 泄漏/大缓存 → dump → MAT 找 Path to GC Roots
└─ 回落 → 分配太快/晋升太早 → 看 YGC 频率、Survivor 大小
│
③ 定位触发源
├─ 元空间 → 动态类
├─ 显式 gc → 代码或框架
└─ 晋升失败 → 担保、碎片、大对象
│
④ 处置
短期:扩容/限流/重启(保业务)
长期:修代码/换收集器/调参(治根)
三、现场命令
# 1. 频率与回收效果(每 1 秒采一次,连采 60 次)
jstat -gcutil 1234 1000 60
# 列含义:S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 关注:O 的走势、FGC 是否在增长、FGCT 单次均值
# 2. 抓到 Full GC 前后的对象分布
jmap -histo:live 1234 | head -25
# 3. 导出堆,离线分析
jcmd 1234 GC.heap_dump /tmp/fullgc.hprof
# 4. 查显式 GC 的调用栈(Arthas)
# watch java.lang.System gc '{params, returnObj, throwExp}' -x 2 -n 5
四、常见处置与代价
| 处置 | 立刻见效 | 代价 |
|---|---|---|
| 扩大堆 | 是 | 单次停顿更长,泄漏仍在,只是推迟 |
| 降低缓存容量 / 加淘汰 | 是 | 命中率下降,回源压力上升 |
| 修泄漏(断开引用) | 是 | 需要定位与发版 |
| CMS 换 G1 | 是 | 需要压测验证停顿目标 |
| 提前 CMS 阈值 | 部分 | 更频繁的并发收集,CPU 占用上升 |
关闭 System.gc() | 视情况 | 可能影响堆外内存清理时机 |
五、一个典型的误判
「堆从 8G 扩到 16G 后 Full GC 少了」,看起来解决了,实际上泄漏只是被更大的空间掩盖:对象增长曲线不变,爆发时间点只是推迟了一倍。判断是否真正解决,要看单位业务量的老年代增长速率是否下降,而不是 Full GC 次数。
# 计算「每秒晋升量」:连续两次采样老年代已用字节数之差 / 时间
jstat -gc 1234 5000 10 | awk 'NR>1 {if (prev) printf "晋升速率 %.1f MB/s\n", ($6+$8-prev)/5/1024; prev=$6+$8}'
六、常见追问
| 追问 | 回答要点 |
|---|---|
| Full GC 频繁但每次都很短,要紧吗 | 要紧,说明老年代在反复扫,CPU 被持续消耗,尾延迟会抖 |
| 能否在线不重启解决 | 多数不能,但可以限流、临时扩堆、关闭缓存写入;根治仍需发版 |
| 怎么证明是代码泄漏 | dump 中同一批对象持续增长、且 Path to GC Roots 指向业务静态容器 |
| G1 下 Full GC 有什么特别 | 常见原因是 Humongous 分配失败或转移失败,重点查大数组与 G1ReservePercent |
| 监控该配什么指标 | Full GC 次数与停顿、老年代使用率、晋升速率、元空间使用率,四个一起看 |
小结
先分清「回收不动」还是「回收太勤」,再顺着触发源定位到元空间、显式调用、晋升失败或泄漏。扩容只能买时间,判断是否真解决,要看单位业务量的内存增长速率。