JVM 内存模型与 GC 日志怎么看
JVM 内存模型与 GC 日志怎么看
线上服务突然变慢,接口 P99 从 50ms 飙到几秒,CPU 却不高。老手第一反应是:看看是不是 GC 出问题了。但很多人打开 GC 日志一看,满屏的 [GC (Allocation Failure)]、Pause Young、一堆数字,完全不知道哪个是关键。
这篇不堆理论,目标很明确:讲清楚 JVM 运行时内存分哪几块、对象在里面怎么流动,然后手把手教你读一段真实的 GC 日志,知道该盯哪几个数字判断健康与否。
JVM 运行时内存分区
先建立一张地图。JVM 把内存分成几块,和 GC 最相关的是堆(Heap):
- 堆(Heap):对象实例都分配在这里,是 GC 的主战场。堆又分成:
- 新生代(Young):新创建的对象先进这里。内部再分 Eden 和两个 Survivor(S0/S1)。绝大多数对象"朝生夕死",在新生代就被回收了。
- 老年代(Old):在新生代熬过多次 GC 还活着的对象,会被"晋升"到老年代。长期存活的对象(如缓存、单例)住这里。
- 元空间(Metaspace):存类的元信息(JDK 8 后从永久代 PermGen 挪到了本地内存)。
- 虚拟机栈 / 本地方法栈 / 程序计数器:线程私有,存方法调用的局部变量、操作数等,和 GC 关系不大。
对象的一生大致是:在 Eden 出生 → Young GC 时把存活对象挪到 Survivor → 熬过若干次 Young GC → 晋升到 Old → Old 满了触发 Full GC。
两种 GC:Young GC 和 Full GC
理解日志前,先分清两类回收:
- Young GC(Minor GC):只回收新生代。触发频繁(几秒到几十秒一次很正常),但速度快,停顿通常在毫秒级。这是健康的。
- Full GC:回收整个堆(含老年代)甚至 Metaspace。停顿长(可能几百毫秒到数秒),而且会 Stop The World——所有业务线程暂停。Full GC 频繁,才是要命的信号。
排查性能问题时,第一件事就是看 Full GC 的频率和耗时。
打开 GC 日志
JDK 9+ 用统一的 -Xlog 参数(JDK 8 的写法在文末补充):
# JDK 9 及以后
java -Xlog:gc*:file=gc.log:time,uptime,level,tags \
-Xms512m -Xmx512m -jar app.jar
# JDK 8
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:gc.log -jar app.jar
-Xms 和 -Xmx 建议设成相等,避免堆动态扩缩容带来的额外停顿和干扰,也让日志更好读。
逐行读一段真实日志(G1 GC,JDK 11)
下面是 G1 收集器的一段典型输出,我们逐段拆:
[2026-07-09T10:15:32.101+0800][3.204s] GC(12) Pause Young (Normal) (G1 Evacuation Pause)
[2026-07-09T10:15:32.101+0800][3.204s] GC(12) Eden regions: 24->0(24)
[2026-07-09T10:15:32.101+0800][3.204s] GC(12) Survivor regions: 3->3(4)
[2026-07-09T10:15:32.101+0800][3.204s] GC(12) Old regions: 10->12
[2026-07-09T10:15:32.101+0800][3.204s] GC(12) Humongous regions: 0->0
[2026-07-09T10:15:32.102+0800][3.205s] GC(12) Pause Young (Normal) 195M->98M(512M) 8.451ms
关键信息这样读:
GC(12):第 12 次 GC 事件,编号方便你追踪同一次 GC 的多行输出。Pause Young (Normal):这是一次普通的新生代回收——好现象。如果看到Pause Full,才要警惕。Eden regions: 24->0(24):Eden 从占用 24 个 region 清成 0,说明新对象都被处理了(存活的挪走、死的回收)。Old regions: 10->12:老年代从 10 涨到 12 个 region,说明有 2 个 region 的对象晋升到了老年代。持续快速上涨要留意,是 Full GC 的前兆。195M->98M(512M):这是最该盯的一行。GC 前堆用了 195M,GC 后降到 98M,堆总大小 512M。回收掉了约 97M。8.451ms:本次停顿 8.4 毫秒。Young GC 在毫秒级是正常的。
判断健康的几个信号
把上面的数字串起来,健康的 GC 应该满足:
- GC 后堆占用能明显回落。像
195M->98M,回收后降回低位,说明内存能有效释放。如果是480M->470M——回收了个寂寞,内存基本没降,老年代快满了,离 Full GC 不远。 - 老年代增长平缓。
Old regions每次 Young GC 只涨一点点是正常的;如果每次都猛涨,说明对象晋升太快,可能是新生代太小或有大对象。 - Full GC 稀少。理想情况几小时甚至几天一次。如果日志里
Pause Full几分钟就来一次,几乎可以断定有内存问题。 - 单次停顿时间可控。Young GC 毫秒级、Full GC 别超过几百毫秒。
一个典型的内存泄漏信号长这样——每次 Full GC 后堆占用不降反升:
Pause Full ... 500M->480M(512M) 820ms # 回收后还剩 480M
Pause Full ... 505M->495M(512M) 910ms # 越回收剩得越多,停顿越长
Pause Full ... 510M->505M(512M) 1050ms # 马上就要 OOM 了
这种情况别再纠结 GC 参数,直接上 jmap -dump:format=b,file=heap.bin <pid> 导出堆快照,用 MAT(Memory Analyzer)分析是谁在占内存不放。
快速上手的排查命令
不想翻日志时,几个命令能快速看现状:
# 实时看各代内存和 GC 次数/耗时,每 1 秒刷一次
jstat -gcutil <pid> 1000
# 输出里 YGC/YGCT 是 Young GC 次数/总耗时,FGC/FGCT 是 Full GC 次数/总耗时
# 重点看 FGC 是不是在快速增长,O(老年代使用率%)是不是一直贴近 100
# 看堆里对象数量和大小排名,快速定位可疑的大户
jmap -histo:live <pid> | head -20
jstat -gcutil 里如果 FGC 每隔几秒就 +1、O 长期在 95% 以上,基本实锤是老年代回收不动了。
小结
- 堆分新生代(Eden + 2 Survivor) 和老年代;对象在 Eden 出生,熬过多次 Young GC 后晋升老年代。
- Young GC 频繁但快是正常的;Full GC 频繁才是危险信号,它会 Stop The World。
- 读日志最该盯
GC前->GC后(总量)这个数:回收后能明显回落 = 健康,回收后基本不降 = 快满/泄漏。 - 老年代持续快速上涨、Full GC 后内存不减反增,是内存泄漏的典型特征,用
jmapdump + MAT 定位元凶。 - 一句话记忆:GC 日志只看三样——Full GC 频不频、回收后堆降不降、单次停顿长不长。
更多推荐



所有评论(0)