Skip to content

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 FailedYoung 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频繁。

bash
# 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 GC

FGC在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: 看老年代趋势

bash
$ 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 GC

Step 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。

方案:

bash
# 方案A: 增加新生代大小(推荐)
-Xmn4g                   # 当前2g → 4g
-XX:SurvivorRatio=6      # 增大Survivor比例(从8→6)

# 方案B: 增大晋升年龄阈值
-XX:MaxTenuringThreshold=15   # 让对象在Survivor多待几次GC

Step 7: 验证

bash
# 调优后再次检查
$ 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 causeSystem.gc()jstat -gccause+DisableExplicitGC
MetaspaceMC/MU接近最大值Metadata GC Threshold类加载统计MaxMetaspaceSize + 排查类泄漏
Concurrent Mode FailureCMS收集未完成Old就满Concurrent Mode Failure增大Old或减小并发线程+UseG1GC或换GC
Promotion FailedYoung GC后对象无法晋升Promotion FailedOld空间/碎片增大Old或减少Young
Humongous Allocation (G1)G1无法分配大对象Humongous Allocation大对象大小-XX:G1HeapRegionSize
GC LockerJNI临界区GC LockerJNI调用减少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类似亚毫秒

关键调优参数

bash
# 通用
-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的最佳实践

  1. 设置监控告警: jstat -gcutil定期采集,FGC超过阈值报警
  2. GC日志监控: 自动收集GC日志,出现Full GC自动分析
  3. 调整初始化参数: -Xms = -XmxMetaspaceSize = MaxMetaspaceSize,避免扩容GC
  4. 禁用System.gc(): 生产环境加-XX:+DisableExplicitGC
  5. 使用G1或ZGC: JDK 17+推荐ZGC,JDK 11+推荐G1
  6. 合理的堆大小: 不要超过物理内存的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堆** - 堆结构

Knowledge4J — Java 知识库