06.6 - Full GC频繁排查方法论
定位: Full GC是生产环境最隐蔽的性能杀手——它不直接报错,但拖慢一切 面试高频度: ⭐⭐⭐⭐⭐
一、为什么Full GC是"慢性杀手"?
Full GC = Stop-The-World 暂停。
暂停期间,所有业务线程停止。暂停时间短的几百毫秒,长的可能几十秒。
| 影响 | 表现 | 后果 |
|---|---|---|
| 接口延迟 | 单次暂停 300ms-5s | 上游超时重试 |
| 线程池积压 | 暂停期间任务堆积 | 恢复后CPU波峰 |
| 健康检查失败 | 超过探针超时 | 被K8s/注册中心踢出 |
| 连接池耗尽 | 暂停期间连接未释放 | RestTemplate/DB连接池满 |
关键认知: Full GC不是"偶尔发生一次就没事"——一次Full GC暂停1秒,之后系统需要时间恢复,如果业务高峰期持续出现,系统就在"GC → 恢复 → 堆满 → 再GC"的振荡中。
二、Full GC的前置理解
常见的Full GC触发原因
Full GC触发原因决策树
┌────────────────────────────┐
│ Full GC触发了 │
└────────────┬───────────────┘
│
┌───────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ 老年代满了 │ │ System.gc() │ │ GC locker/JNI │
│ <- 最常⻅原因 │ │ <- 显式调用 │ │ <- JDK bug │
└──────┬───────┘ └──────┬───────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Metaspace满 │ │ Promotion Failed │
│ 类加载太多 │ │ 晋升时Old空间不足│
└──────────────┘ └──────────────────┘2.1 老年代满了 — 最常见
老年代填满时,JVM必须执行Full GC来腾出空间。在G1中表现为"Mixed GC",在CMS中表现为"Concurrent Mode Failure"。
2.2 System.gc() — 最坑爹
代码中显式调用System.gc()会触发Full GC。常见于:
- NIO/DirectByteBuffer的
System.gc()调用 - RMI框架的定时Full GC(默认每小时一次)
- JNI代码调用
- 某些框架的"内存清理"逻辑
解决方案: -XX:+DisableExplicitGC会忽略System.gc()。
但注意:-XX:+DisableExplicitGC同时也阻止了DirectByteBuffer的回收触发。需要权衡。生产环境通常开启。
2.3 Metaspace满了 — 类加载过多
元空间使用量达到-XX:MetaspaceSize阈值后,会触发Full GC来卸载类。如果元空间持续增长(类加载泄漏),Full GC也会持续。
解决方案: -XX:MaxMetaspaceSize=256m设定上限,同时排查类加载泄漏。
2.4 Promotion Failed / Concurrent Mode Failure
| 模式 | 场景 | 特征 |
|---|---|---|
| Promotion Failed | Young GC后对象要晋升Old,但Old空间不足 | Full GC前会打印"Promotion Failed" |
| Concurrent Mode Failure (CMS) | CMS并发收集阶段Old又被填满 | "Concurrent Mode Failure" → 退化为Serial Old GC |
| Evacuation Failure (G1) | G1复制对象时Region空间不足 | "Evacuation Failure" → 触发G1 Full GC |
三、Full GC诊断七步法
┌────────────────────────────────────────────────────────────────────────────┐
│ Full GC诊断流程 │
├────────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 确认情况 │
│ $ jstat -gcutil <pid> 1000 │
│ FGC快速增⻓ × FGC时间⻓ → 确认Full GC │
│ │
│ Step 2: 记录GC日志 → 分析触发原因 │
│ 确认打印了GC日志,关注每次Full GC前的"Cause" │
│ │
│ Step 3: 看老年代使用率趋势 │
│ $ jstat -gc <pid> 1000 10 │
│ OU列是否每次采样都在上涨? │
│ │
│ Step 4: 看晋升速率 │
│ -XX:+PrintTenuringDistribution + GC日志分析 │
│ 晋升速率(GB/min) = 每次Young GC后Old增加量 × YGC频率 │
│ │
│ Step 5: 确定根因类别 │
│ OU上涨过快 → 晋升量 > Old回收量 │
│ OU平稳但FGC增加 → 检查System.gc() │
│ MC上涨太快 → Metaspace问题 │
│ │
│ Step 6: 针对性修复 │
│ ├── Young太小 → 增大Xmn或调整NewRatio │
│ ├── 晋升太快 → 增大SurvivorRatio或MaxTenuringThreshold │
│ ├── 大对象直接进入Old → -XX:PretenureSizeThreshold防止TLAB跳过 │
│ ├── System.gc() → +DisableExplicitGC │
│ ├── Metaspace → 设置MaxMetaspaceSize,排查类加载 │
│ └── GC选型不当 → 调整GC或换GC │
│ │
│ Step 7: 验证效果 │
│ 同压力条件下,FGC不再增长或频率显著降低 │
│ │
└────────────────────────────────────────────────────────────────────────────┘四、实战案例:完整的Full GC排查
场景
在线支付服务告警:延迟从50ms飙到5s。检查发现Full GC频繁。
# Step 1: 确认Full GC
$ jstat -gcutil <pid> 1000
S0 S1 E O M YGC YGCT FGC FGCT GCT
0.00 14.35 85.20 78.50 85.00 500 3.000 10 8.000 11.000
0.00 14.35 86.10 79.50 85.00 501 3.002 11 8.800 11.802 ← FGC+1!
0.00 14.35 87.00 80.20 85.00 502 3.004 12 9.600 12.604 ← 10秒3次Full GCFGC在10秒内从10增加到12,每次FGCT约0.8秒——确认Full GC频率高。
Step 2: 分析GC日志
[Full GC (Allocation Failure) 8000M->6000M(8192M), 0.850 secs]
[Eden: 0.0B(1000.0K)->0.0B(1000.0K) Survivors: 0.0B->0.0B Heap: 8.0G(8.0G)->6.0G(8.0G)]
[Times: user=0.85 sys=0.00, real=0.85 secs]Full GC后堆从8000M降到6000M——回收了2000M但留下了6000M。这6000M就是"顽固对象"。
Step 3: 看老年代趋势
$ jstat -gc <pid> 1000 5
S0C S1C S0U S1U EC EU OC OU
512.0 512.0 0.0 70.0 4608.0 4000.0 10240.0 8000.0 ← OU=8000M
512.0 512.0 0.0 72.0 4608.0 4100.0 10240.0 8100.0 ← OU持续增⻓
512.0 512.0 0.0 68.0 4608.0 3800.0 10240.0 8200.0 ← 每个采样都在涨OU持续上涨:说明晋升速率 > 回收速率。
Step 4: 计算晋升速率
从GC日志看:
Young GC前 Old=7000M
Young GC后 Old=7100M(多了100M)
YGC间隔=200ms,每次YGC晋升100M
晋升速率 = 100M / 0.2s = 500MB/s
这么快的晋升速率,Old很快就会满 → Full GCStep 5: 检查晋升原因
使用-XX:+PrintTenuringDistribution获取年龄分布:
Desired survivor size 1048576 bytes, new threshold 1 (max 6)
- age 1: 1048576 bytes, 1048576 total
- age 2: 524288 bytes, 1572864 total
- age 3: 262144 bytes, 1835008 total
- age 4: 0 bytes, 1835008 total
- age 5: 0 bytes, 1835008 total
: 3665920 bytes, 3665920 total
...年龄分布显示:大部分对象在Young GC后进入age 1时就达到了晋升阈值——说明Survivor太小,放不下存活对象。
Step 6: 修复
问题: Survivor太小(默认Eden:Survivor=8:1),大量存活对象直接晋升到Old。
方案:
# 方案A: 增加新生代大小(推荐)
-Xmn4g # 当前2g → 4g
-XX:SurvivorRatio=6 # 增大Survivor比例(从8→6)
# 方案B: 增大晋升年龄阈值
-XX:MaxTenuringThreshold=15 # 让对象在Survivor多待几次GCStep 7: 验证
# 调优后再次检查
$ jstat -gcutil <pid> 1000
S0 S1 E O M YGC YGCT FGC FGCT GCT
20.0 0.00 40.0 50.0 85.0 2000 12.0 0 0.0 12.0 ← FGC=0!Full GC停止,Old使用率稳定在50%不再上涨。
五、Full GC触发原因对照表
| 触发原因 | 症状 | GC日志关键词 | 排查方向 | 修复方案 |
|---|---|---|---|---|
| 晋升太快 | OU快速上涨 | Allocation Failure | 晋升速率计算 | 增大Xmn/Survivor |
| 内存泄漏 | 堆整体上涨 | 每次Full GC只回收一点点 | heap dump分析 | 修代码 |
| System.gc() | 日志里有System.gc cause | System.gc() | jstat -gccause | +DisableExplicitGC |
| Metaspace | MC/MU接近最大值 | Metadata GC Threshold | 类加载统计 | MaxMetaspaceSize + 排查类泄漏 |
| Concurrent Mode Failure | CMS收集未完成Old就满 | Concurrent Mode Failure | 增大Old或减小并发线程 | +UseG1GC或换GC |
| Promotion Failed | Young GC后对象无法晋升 | Promotion Failed | Old空间/碎片 | 增大Old或减少Young |
| Humongous Allocation (G1) | G1无法分配大对象 | Humongous Allocation | 大对象大小 | -XX:G1HeapRegionSize |
| GC Locker | JNI临界区 | GC Locker | JNI调用 | 减少JNI临界区时间 |
六、GC调优参数速查
六种收集器的Full GC特点
| 收集器 | 并发性 | Full GC特点 | 暂停时间 |
|---|---|---|---|
| Serial | 串行 | 完整STW,单线程 | 数秒-数十秒 |
| Parallel | 串行 | 完整STW,多线程 | 数秒 |
| CMS | 并发 | Concurrent Mode Failure时退化为Serial Old | 毫秒-秒(退化后很长) |
| G1 | 并发 | Mixed GC / 退化Full GC | 毫秒-秒 |
| ZGC | 并发 | 几乎没有Full GC | 亚毫秒 |
| Shenandoah | 并发 | 同ZGC类似 | 亚毫秒 |
关键调优参数
# 通用
-Xms -Xmx # 堆大小
-Xmn # 新生代大小
-XX:NewRatio # 老年代:新生代比例
-XX:SurvivorRatio # Eden:Survivor比例
-XX:MaxTenuringThreshold # 最大晋升年龄
# G1专用
-XX:G1HeapRegionSize # Region大小(1-32MB)
-XX:InitiatingHeapOccupancyPercent # 启动并发GC的堆占用率(default 45%)
-XX:G1MixedGCLiveThresholdPercent # Mixed GC存活对象阈值
-XX:G1NewSizePercent # G1新生代初始占比
# Parallel专用
-XX:ParallelGCThreads # 并行GC线程数
-XX:GCTimeRatio # 吞吐量目标(1/(1+ratio))
-XX:UseAdaptiveSizePolicy # 自适应调整详见 **JVM参数分类速查** 和 **GC调优参数速查**。
七、预防Full GC的最佳实践
- 设置监控告警:
jstat -gcutil定期采集,FGC超过阈值报警 - GC日志监控: 自动收集GC日志,出现Full GC自动分析
- 调整初始化参数:
-Xms = -Xmx、MetaspaceSize = MaxMetaspaceSize,避免扩容GC - 禁用System.gc(): 生产环境加
-XX:+DisableExplicitGC - 使用G1或ZGC: JDK 17+推荐ZGC,JDK 11+推荐G1
- 合理的堆大小: 不要超过物理内存的70%,为操作系统和堆外内存留空间
八、面试视角
高频面试题
| 面试题 | 考察点 | 回答要点 |
|---|---|---|
| "Full GC频繁怎么排查?" | 方法论 | 七步法:确认→日志→OU趋势→晋升速率→根因→修复→验证 |
| "System.gc()会触发什么?" | GC理解 | 触发Full GC。NIO框架用来回收DirectByteBuffer |
| "如何减少Full GC?" | 调优思路 | 减少晋升(增大Survivor)、减少System.gc()、选择合适的GC |
| "CMS的Concurrent Mode Failure是什么?" | CMS理解 | CMS并发收集时Old又被填满,退化为Serial Old Full GC |
| "G1的Mixed GC和Full GC有什么区别?" | G1理解 | Mixed GC是G1的正常并发回收,Full GC是退化的单线程STW |
| "晋升速率怎么算?" | 数据分析 | (每次YGC后Old的增长量) × (YGC频率) |
| "年轻代越大越好吗?" | 辩证思维 | 不是。Young太大 → Minor GC间隔长但暂停时间长 + Old空间减少 |
面试回答模板
Q: 线上Full GC频繁,你怎么排查? A: 我会按七步走。第一步
jstat -gcutil确认Full GC确实频繁且暂停时间长。第二步看GC日志触发原因。第三步看老年代趋势:OU是不是持续上涨。第四步通过GC日志分析晋升速率。如果晋升速率远超回收速率,说明Young或Survivor太小,对象过早晋升。增大Xmn或调整SurvivorRatio。如果OU不涨但FGC多,可能是System.gc()——用jstat -gccause确认,然后加DisableExplicitGC。
Q: G1的Full GC什么时候触发? A: G1的Full GC触发场景包括:1) 并发标记完成后Mixed GC来不及回收(堆太小);2) Humongous对象分配失败(Region无法放下);3) Evacuation Failure(复制对象时目标Region不足);4) Metadata GC Threshold(元空间满)。G1的Full GC是退化的——回到单线程串行Full GC,暂停时间很长,应尽量避免。
📚 相关链接
- ← 返回 **调优索引**
- **JVM参数分类速查** - 参数细表
- **OOM排查方法论** - GC thrashing与OOM关联
- **垃圾收集器详解(Serial到ZGC)** - 各收集器原理
- **GC日志分析** - GC日志解读
- **GC调优参数速查** - GC专用参数
- **Java堆** - 堆结构