FullGC 多久一次正常?
结论先行:没有统一的「正常频率」,判断标准是趋势与停顿占比,而不是次数。一个健康系统通常天级甚至周级才出现一次 Full GC;如果每小时都有,就要查原因;如果每分钟都有,基本可以确定存在泄漏或元空间膨胀。真正该盯的指标是「Full GC 停顿时间占运行时间的比例」,一般要求低于 0.1%。
一、按频率判断
| 频率 | 判断 | 常见原因 |
|---|---|---|
| 数天 / 数周一次 | 正常 | 老年代自然填满或元空间缓慢增长 |
| 每天数次 | 可接受,需观察 | 缓存预热、大促流量峰值、晋升速率偏高 |
| 每小时数次 | 异常 | 老年代增长快、Survivor 过小导致过早晋升 |
| 每分钟数次 | 严重 | 内存泄漏、显式 System.gc()、元空间不足反复扩容 |
| 持续循环 | 灾难 | 堆几乎全是存活对象,GC 无效 |
二、Full GC 的触发来源
Full GC 触发
├── 老年代空间不足(晋升失败 / 担保失败)
├── 元空间(Metaspace)达到阈值需要扩容
├── 显式调用:System.gc()、Runtime.getRuntime().gc()
├── CMS 的并发模式失败(concurrent mode failure)
├── G1 的转移失败(evacuation failure)
├── 堆转储(jmap -dump / HeapDumpBeforeFullGC)
└── 直接内存不足触发的 System.gc()(DirectByteBuffer 清理)
三、用日志算频率
# 统计 Full GC 次数、总停顿、单次最长停顿
grep -E "Pause Full|Full GC" /var/log/app/gc.log \
| awk '{for(i=1;i<=NF;i++) if($i ~ /ms$/) print $(i-1), $i}' \
| awk '{n++; sum+=$1; if($1>max) max=$1} END {printf "次数=%d 总停顿=%dms 均值=%.1fms 最长=%dms\n", n, sum, sum/n, max}'
# 每秒采样一次,观察老年代占用与 FGC 计数
jstat -gcutil 1234 1000 20
# 关注列:O(老年代使用率)、FGC(Full GC 次数)、FGCT(累计停顿秒)
# 直接看所有 GC 原因的累计次数(JDK 11+)
jcmd 1234 GC.cause
jstat 输出里如果 O 长期停在 95% 以上、FGC 每轮都涨,且 FGCT 同步增长,就是「回收不动」的典型信号。
四、把 Full GC 打回原形
public class FullGcDemo {
private static final java.util.List<byte[]> HOLD = new java.util.ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
HOLD.add(new byte[4 * 1024 * 1024]); // 持续持有,逼近 -Xmx
System.out.println("allocated " + (i + 1) * 4 + "MB");
}
System.gc(); // 显式触发,一次就能在日志里看到 Pause Full
}
}
// 运行:java -Xmx64m -Xlog:gc FullGcDemo
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 元空间为什么会触发 Full GC | MetaspaceSize 是首次扩容阈值,达到后先做一次 Full GC 尝试回收,回收不了才扩容 |
System.gc() 怎么查来源 | -XX:+ExplicitGCInvokesConcurrent 改为并发;用 -Xlog:gc+heap=debug 或 Arthas stack 抓调用栈 |
| CMS 的并发模式失败是什么 | 并发清理还没结束老年代就满了,只能退化为单线程 Full GC,停顿极长 |
| G1 也会 Full GC 吗 | 会,转移失败或 Humongous 分配失败时会退化为 Serial Full GC,这是 G1 需要重点监控的场景 |
| Full GC 后老年代不降说明什么 | 说明对象真的还活着:要么是长生命周期缓存,要么是泄漏,需要 dump 确认 |
六、日志配置别忘版本差异
| 目的 | JDK 8 | JDK 9+ |
|---|---|---|
| 打开 GC 日志 | -XX:+PrintGCDetails -XX:+PrintGCDateStamps | -Xlog:gc* |
| 输出到文件并滚动 | -Xloggc:/var/log/gc.log -XX:+UseGCLogFileRotation | -Xlog:gc*:file=/var/log/gc.log:filesize=50m,filecount=5 |
| 记录 GC 原因 | 无对应开关,靠日志文本判断 | -Xlog:gc+heap=debug、jcmd GC.cause |
| 记录停顿细节 | -XX:+PrintGCApplicationStoppedTime | -Xlog:safepoint |
没有日志就没有「多久一次」这个问题的答案。很多团队只在事故后才想起加参数,结果重启丢失现场,只能猜。
小结
别背「多久一次」,要会解释「为什么是这个频率」。把 Full GC 次数、停顿、老年代回收率三组数据一起看,再对着触发来源逐项排除,结论自然清楚。