堆空间 GC 举例与日志分析
GC 日志:JVM 的黑匣子
GC 日志记录了每一次垃圾回收的详细信息。学会读 GC 日志,是排查 GC 问题和性能调优的基础。
开启 GC 日志
bash
# JDK 8 及之前:常用参数
java -Xms256m -Xmx256m \
-XX:+PrintGCDetails \ # 打印详细 GC 信息
-XX:+PrintGCDateStamps \ # 打印时间戳
-XX:+PrintTenuringDistribution \ # 打印晋升年龄分布
-Xloggc:/tmp/gc.log \ # 输出到文件
MyApp
# JDK 9+:统一日志框架
java -Xms256m -Xmx256m \
-Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags \
MyAppMinorGC 日志解析
bash
# JDK 8 风格的 MinorGC 日志:
2026-03-22T10:15:30.123+0800: [GC (Allocation Failure)
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age 1: 1048576 bytes, 1048576 total
eden space 2621440K, 2595328K used (99%)
from space 524288K, 0K used (0%)
to space 524288K, 0K used (0%)
: 2595328K->1048576K(3145728K), 0.0234567 secs]
[Times: user=0.10 sys=0.01, real=0.02 secs]关键字段解读
eden space 2621440K, 2595328K used (99%)
↑ Eden 区总大小 2.5GB,使用了 99%
from space 524288K, 0K used (0%)
to space 524288K, 0K used (0%)
↑ 两个 Survivor 区,S0 和 S1
↑ MinorGC 后,对象从 Eden 和 From Survivor 复制到 To Survivor
2595328K->1048576K(3145728K)
↑ GC 前占用 2595MB,GC 后占用 1048MB
↑ 括号内 3145728K = 年轻代总大小
0.0234567 secs
↑ 停顿时间 23.5 毫秒
[Times: user=0.10 sys=0.01, real=0.02]
↑ user=0.10s: GC 线程消耗的 CPU 时间(所有 GC 线程累加)
↑ sys=0.01s: OS 调用消耗的时间
↑ real=0.02s: 实际墙钟时间FullGC 日志解析
bash
# FullGC 日志:
2026-03-22T10:15:45.678+0800: [Full GC (Allocation Failure)
[CMS: 1572864K->1468006K(1572864K), 1.2345678 secs]
1572864K->1468006K(2097152K), 2.3456789 secs]
[Times: user=3.45 sys=0.12, real=2.35 secs]关键字段解读
Full GC (Allocation Failure)
↑ 触发原因:分配失败(老年代空间不足)
[CMS: 1572864K->1468006K(1572864K), 1.23 secs]
↑ CMS 收集器的收集结果
↑ 老年代从 1572864K(1.5GB) 降到 1468006K(1.4GB)
↑ 括号内是老年代的容量
↑ 停顿了 1.23 秒
1572864K->1468006K(2097152K)
↑ 整个堆从 1572864K 降到 1468006K
↑ 括号内是堆的总容量
2.3456789 secs
↑ FullGC 总停顿时间 2.35 秒(较长!)
user=3.45 sys=0.12, real=2.35
↑ 多线程 GC,user 时间 > real 时间G1 GC 日志解析
G1 的日志与其他 GC 收集器有较大不同:
bash
# G1 的 MinorGC
2026-03-22T10:16:00.123: [GC pause (G1 Evacuation Pause) (young)
Using 8 workers
, 0.0123456 secs]
[Eden: 1536.0M(1536.0M)->0.0B
Survivors: 96.0M->128.0M
Heap: 4096.0M(4096.0M)->2048.0M(4096.0M)]
# G1 的 MixedGC
2026-03-22T10:16:30.456: [GC pause (G1 Evacuation Pause) (mixed)
[Eden: 1024.0M(1024.0M)->0.0B
Survivors: 64.0M->32.0M
Heap: 4096.0M(4096.0M)->2560.0M(4096.0M)]
[Choose CSet: 512.0M]G1 日志特点
G1 把堆划分为多个 Region,日志中可以看到 Eden/Survivor/Humongous 类型的 Region。
- young:纯年轻代收集
- mixed:混合收集,同时回收年轻代和部分老年代
- initial mark:并发标记的开始
- remark:并发标记的结束
实战案例:识别 GC 问题
案例一:频繁 FullGC,内存不断增长
[Full GC (Allocation Failure)
1000K -> 999K (1000K), 0.100 secs]
[Full GC (Allocation Failure)
1010K -> 1009K (1000K), 0.100 secs]
[Full GC (Allocation Failure)
1020K -> 1019K (1000K), 0.100 secs]诊断:每次 FullGC 后内存几乎没降,说明有大量对象无法回收——典型的内存泄漏或大对象问题。
案例二:MinorGC 后大量对象晋升
[GC (Allocation Failure)
eden 500M->50M, S0 50M->50M(50M), age 1: 80M, S0 满了, age >= 15: 200M]诊断:Survivor 区满了(S0->S0(50M)),大量年龄>=1的对象(80M)无法进入 Survivor,被强制晋升老年代。
解决方案:增大 Survivor 区(-XX:SurvivorRatio)或降低晋升阈值。
案例三:GC 时间过长
[Full GC: 4000M->3500M(4000M), 5.678 secs]诊断:FullGC 停顿 5.7 秒,这对在线服务是致命的。
解决方案:
- 减少堆大小(减小 GC 扫描范围)
- 升级 GC 收集器(G1、ZGC)
- 减少大对象直接进入老年代
GC 日志分析工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| GCEasy | 在线工具,上传 gc.log,自动分析 | 快速定位问题 |
| GCViewer | 开源桌面工具 | 本地离线分析 |
| IBM PMAT | IBM GC 专用 | 分析 IBM J9 GC 日志 |
| jstat | JDK 内置命令行工具 | 实时监控 |
| Arthas | 阿里开源,命令行 | 线上诊断 |
bash
# jstat:实时查看 GC 统计
jstat -gcutil <pid> 1000
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
# 0.00 52.00 65.00 78.00 92.00 88.00 123 2.34 5 12.34 0 0.00 14.68本节小结
GC 日志分析的核心要点:
| 指标 | 含义 |
|---|---|
| Eden/S0/S1/O | 各区域的内存使用率 |
| GC Time | GC 停顿时间 |
| User/Sys/Real | CPU时间/OS时间/墙钟时间 |
| Allocation Failure | 触发原因:分配失败 |
通过 GC 日志,你可以发现:
- GC 频率:是否过于频繁
- GC 时间:是否停顿过长
- 内存趋势:是否持续增长
- 晋升情况:是否有异常晋升
下一节,我们来看方法区(元空间)的核心内容——方法区概述(与栈/堆交互)。
