Skip to content

05.4 - GC 日志分析

定位: 生产环境中诊断 GC 问题的唯一窗口——没有 GC 日志,排查就是"盲人摸象" 面试高频度: ⭐⭐⭐⭐ 考查方式: 给出 GC 日志让你解读、Full GC 频繁怎么排查、JDK 8 和 JDK 11 的日志区别

一、这是什么?为什么需要它?

**垃圾收集器详解(Serial到ZGC)** 介绍了各收集器的工作原理。但生产环境中的 GC 行为和理论可能完全不同——对象分配速率、Survivor 溢出、过早晋升、并发模式失败等,都需要通过日志来发现。

GC 日志是 JVM 内存行为的黑盒子记录器

GC 日志的作用域:
┌─────────────────────────────────────────────────────────┐
│                     生产运维链路                           │
│                                                         │
│  GC 日志  ──→  GCViewer/GCeasy  ──→  分析报告  ──→  调优 │
│    ↑                                ↑                   │
│  持续的日志记录                   发现问题:                │
│  (滚动文件, 10份 × 100M)          Full GC 频繁          │
│                                   停顿时间过长            │
│                                   CPU 占用过高           │
└─────────────────────────────────────────────────────────┘

二、原理拆解

2.1 JDK 8 的 GC 日志格式

JDK 8 使用 -XX:+PrintGCDetails 等独立的参数开关(不同参数控制不同输出)。

bash
# JDK 8 典型 GC 参数
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:gc.log

Minor GC 日志拆解

2026-06-15T10:00:01.234+0800: 0.456: [GC (Allocation Failure)
  [PSYoungGen: 65536K->10240K(76288K)]   ← Young: 64M→10M (总76M)
  131072K->55768K(251392K),              ← 总堆: 128M→54M (总246M)
  0.0089732 secs]                         ← 耗时: 8.9ms
  [Times: user=0.03 sys=0.00, real=0.01 secs] ← CPU/真实时间

  ↑                                      ↑
  时间戳 + JVM 启动后秒数                GC 原因: 分配失败 (Eden 满了)

逐字段解读:

[GC (Allocation Failure)                         ← 原因: 分配失败 (Eden 满)
  [PSYoungGen:                                    ← 收集器: Parallel Young
    65536K->10240K(76288K)]                       ← Eden+一个Survivor: 64M→10M (总76M)
  131072K->55768K(251392K),                       ← 整个堆: 128M→54M, 堆总大小 246M
  0.0089732 secs]                                  ← 耗时 9ms
[Times: user=0.03 sys=0.00, real=0.01 secs]       ← user=CPU 时间, real=墙上时间

关键观察:Young 区从 64M 降到 10M(回收了 54M),总堆从 128M 降到 55M(额外回收了来自老年代的 73M?不——这是因为 131072K 是 GC 前的堆占用,但 PSYoungGen 的数值已经包含在总堆中)。

Full GC 日志拆解

2026-06-15T10:01:00.000+0800: 1.567: [Full GC (Ergonomics)
  [PSYoungGen: 20480K->0K(76288K)]               ← Young 清空
  [ParOldGen: 131072K->151552K(175104K)]          ← Old: 128M→148M
  151552K->151552K(251392K),                      ← 堆无变化 (对象全部存活)
  [Metaspace: 4520K->4520K(1056768K)]
  0.5430976 secs]                                  ← 耗时 543ms!
  [Times: user=1.43 sys=0.02, real=0.54 secs]

关键观察:Full GC 耗时 543ms,是老年代对象太多、需要整理引用导致的。Full GC 后堆占用不变 → 所有对象都存活 → 可能是 OOM 的前兆。

2.2 JDK 9+ 统一 GC 日志

JDK 9 引入 Unified JVM Logging-Xlog),一个统一的框架管理所有 JVM 日志。

bash
# JDK 9+ 典型 GC 参数
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=10,filesize=100M

格式说明:

-Xlog:[tag1][,[tag2...]][*][=level] : [output] : [decorators] : [options]
        ↑                           ↑          ↑               ↑
      日志标签                     输出目标   修饰符           文件策略
部分说明示例值
tag日志标签,gc* 表示所有 gc 子标签gc, gc+heap, gc+phases, gc+age, gc+ergo
level日志级别info, debug, trace, warning
output输出目标file=gc.log, stdout, stderr
decorators附加信息time(时间), uptime(启动秒数), level(级别), tags(标签)
options文件策略filecount=10,filesize=100M

常用标签组合

标签内容典型用途
gc*所有 GC 相关日常监控
gc+heapGC 前后堆状态查看堆占用
gc+phasesGC 内各阶段耗时分析停顿组成
gc+age对象年龄分布Survivor 溢出检查
gc+ergoGC 自适应决策为什么涨 Survivor?
gc+promotion对象晋升过早晋升检查
gc+ref引用处理Reference 处理耗时
gc+stringtable字符串表清理String Dedup 效果

JDK 9+ G1 日志拆解

[2026-06-15T10:00:01.234+0800][0.456s][info][gc,start]
  GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)

[2026-06-15T10:00:01.234+0800][0.456s][info][gc,task]
  GC(0) Using 8 workers of 8 for evacuation
                                            ← 8 个 GC 线程并行

[2026-06-15T10:00:01.248+0800][0.470s][info][gc,phases]
  GC(0)   Pre Evacuate Collection Set: 0.0ms     ← 准备阶段
  GC(0)   Evacuate Collection Set: 10.5ms         ← **复制存活对象 10.5ms**
  GC(0)   Post Evacuate Collection Set: 1.2ms     ← 收尾

[2026-06-15T10:00:01.248+0800][0.470s][info][gc,heap]
  GC(0) Eden regions: 32->0(15)                  ← Eden: 32个Region→0个(恢复15个)
  GC(0) Survivor regions: 4->5(5)                ← Survivor: 4→5个Region
  GC(0) Old regions: 120->120                    ← Old: 无变化
  GC(0) Humongous regions: 2->2                  ← 大对象区: 无变化

[2026-06-15T10:00:01.248+0800][0.470s][info][gc,metaspace]
  GC(0) Metaspace: 52.1M(67.2M)->52.1M(67.2M)   ← 元空间无变化

[2026-06-15T10:00:01.248+0800][0.470s][info][gc]
  GC(0) Pause Young (Concurrent Start) 120M->60M(512M) 11.7ms
        ↑                            ↑       ↑       ↑
       GC ID                      GC前→GC后(堆总)  耗时

2.3 关键指标

指标定义正常范围告警阈值
GC 频率每分钟 GC 次数Young: 1-10 次/分钟> 20 次/分钟
YG pauseYoung GC 平均停顿< 50ms> 200ms
FG pauseFull GC 平均停顿< 500ms> 3s
GC 吞吐量应用时间 / (应用+GC)> 99%< 95%
晋升速率每分钟移入 Old 的量< Old 的 1%/分钟> 5%/分钟
堆占用率GC 前堆占用比例< 70%> 85%

2.4 常见模式与诊断

模式 1: 频繁 Young GC
  ┌─ 表现: 每几秒一次 Young GC, 每次杀不了多少
  ├─ 问题: 分配速率太高 (Rate of Allocation)
  ├─ 诊断: 用 jstat -gcutil 看 Eden 使用率增长速度
  └─ 对策: 1) 代码优化(减少不必要的对象创建)
           2) 增大 -Xmn (给新生代更多空间)
           3) 检查是否 TLAB 浪费
    
模式 2: Young GC 后老年代很快被占满 (过早晋升)
  ┌─ 表现: Young GC 后存活对象大量进入 Old
  ├─ 问题: Survivor 太小, 对象来不及死就晋升了
  ├─ 诊断: 打开 -XX:+PrintTenuringDistribution
          观察 age 分布 → 大部分对象在 age 0-1 就晋升
  └─ 对策: 增大 SurvivorRatio → 给 Survivor 更多空间
           调高 MaxTenuringThreshold → 让对象在 Young 多待几轮

模式 3: Full GC 频繁
  ┌─ 表现: Full GC 多次发生, 堆占用居高不下
  ├─ 问题: 老年代迅速填满
  ├─ 诊断: 1) 检查晋升速率 (jstat -gc)
           2) 检查是否有大对象直接分配到 Old
           3) dump 堆分析 (jmap -histo:live)
  └─ 对策: 1) 增加 -Xmx
           2) 优化代码减少长期对象
           3) 检查是否缓存爆炸

模式 4: CMS 并发模式失败
  ┌─ 表现: "Concurrent Mode Failure" → 串行 Full GC
  ├─ 问题: 老年代满的速度 > CMS 清理的速度
  ├─ 诊断: 检查 CMSInitiatingOccupancyFraction → 是否太小?
  └─ 对策: 1) 降低 CMSInitiatingOccupancyFraction (提前触发)
           2) 增大老年代或堆
           3) 考虑切换到 G1

模式 5: 大堆 GC 停顿过长
  ┌─ 表现: Full GC 停顿 > 10s
  ├─ 问题: 堆太大, 移动活着对象耗时太长
  ├─ 诊断: 日志中查看 GC pause duration
  └─ 对策: 1) Parallel → G1
           2) G1 → ZGC (JDK 11+)
           3) 或者把大堆拆成多个进程

三、图解全景

GC 日志分析流程图:

收到告警: "GC 时间过长" / "Full GC 频繁"


        取 GC 日志


        ┌──────────────────────────────────────────────┐
        │  第一步: 看 GC 频率                            │
        │  grep 'GC (' gc.log | head -100               │
        │  看每两次 GC 之间的间隔                          │
        │  → 如果 < 1s → 分配速率太高或堆太小              │
        └──────────────────────────────────────────────┘


        ┌──────────────────────────────────────────────┐
        │  第二步: 看停顿时间                            │
        │  找最长暂停: grep 'Full GC' gc.log            │
        │  看 real= 值, 乘以 GC 线程 / 核数              │
        │  → 如果 > 3s → 堆太大或老年代收集器不合适       │
        └──────────────────────────────────────────────┘


        ┌──────────────────────────────────────────────┐
        │  第三步: 看晋升模式                            │
        │  jstat -gc <pid> 5000 100                     │
        │  观察 Old 区的增速                              │
        │  → 如果 Old 在 Full GC 后迅速增长 → 过早晋升    │
        └──────────────────────────────────────────────┘


        ┌──────────────────────────────────────────────┐
        │  第四步: 对照调优决策树                         │
        │  → 定位到 [05.5-GC调优参数速查] 找对应方案     │
        └──────────────────────────────────────────────┘

四、实战验证

演示: 触发 GC 并查看日志

bash
# JDK 8 运行带 GC 日志的应用
java -Xms100m -Xmx100m \
     -XX:+PrintGCDetails \
     -XX:+PrintGCDateStamps \
     -Xloggc:gc-demo.log \
     -jar myapp.jar
bash
# JDK 17+ 运行带 GC 日志的应用
java -Xms100m -Xmx100m \
     -Xlog:gc*=info:file=gc-demo.log:time,uptime,level,tags:filecount=5,filesize=10m \
     -jar myapp.jar

GCeasy 分析报告关键指标

使用 gceasy.io 上传 GC 日志后,重点关注:

指标含义如果异常
ThroughputGC 吞吐量 (%)< 95% → 需要调优
Avg Pause Time平均停顿> 200ms → 检查收集器
Max Pause Time最大停顿> 1s → 紧急情况
GC Count各类型 GC 次数Full GC > 0 → 需分析原因
GC Cause停顿原因分析Allocation Failure 最常见

五、面试视角

追问答案要点
怎么看 GC 日志?三步法:1) 看 GC 频率(间隔时间);2) 看停顿时间(real=);3) 看堆占用变化(GC 前后对比)。重点关注 Full GC 次数和每次 GC 后是否有空间被释放
Full GC 频繁怎么分析?先确认是谁触发的 Full GC(日志中的 GC Cause)。常见原因:晋升失败、Metaspace 满、System.gc()、CMS 并发模式失败。然后对照晋升速率、老年代增速来定位
JDK 8 和 JDK 11 的 GC 日志有什么区别?JDK 8 用独立参数(-XX:+PrintGCDetails),格式是 [GC [PSYoungGen: ...];JDK 9+ 统一用 -Xlog:gc*,格式带标签和时间戳 [info][gc,start]
如何打开 GC 日志对性能的影响?极小。GC 日志是异步写入的缓冲区操作。但生产环境要注意:日志文件打满磁盘可能导致 JVM 卡死。用滚动策略(filecount+filesize)控制
什么是"过早晋升"(Premature Promotion)?Survivor 空间不足,对象熬不到 MaxTenuringThreshold 就被迫晋升到老年代。导致老年代快速填满,Full GC 频繁。解决:调大 Survivor 或增大 MaxTenuringThreshold
GC 吞吐量怎么算?吞吐量 = 应用运行时间 / (应用运行时间 + GC 总时间)。GC 日志中可以统计所有 GC 的 real 时间之和。99% 以上 = 优秀,95% 以下 = 需要调优
GC 日志中的 user/sys/real 分别代表什么?user=GC 线程消耗的 CPU 时间(多线程 > real),sys=系统调用时间,real=墙上时钟时间(实际暂停时长)
CMS 日志中的"Concurrent Mode Failure"意味着什么?CMS 并发回收速度跟不上应用分配速度,老年代被占满。JVM 放弃 CMS,启用 Serial Old 单线程 Full GC → 停顿时间暴增。根因是老年代预留空间不足或分配速率太高

📚 相关链接

  • **垃圾收集器详解(Serial到ZGC)** — 各收集器的日志特征不同
  • **GC调优参数速查** — 日志参数 + 分析结果后的调优参数
  • **OOM排查方法论** — GC 日志是 OOM 排查的第一步
  • **FullGC频繁排查方法论** — Full GC 的深度排查流程
  • ← 返回 **GC索引**

Knowledge4J — Java 知识库