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*:filegc.log:time,uptime,level,tags\-Xms512m-Xmx512m-jarapp.jar# JDK 8java-XX:PrintGCDetails-XX:PrintGCDateStamps\-Xloggc:gc.log-jarapp.jar-Xms和-Xmx建议设成相等,避免堆动态扩缩容带来的额外停顿和干扰,也让日志更好读。逐行读一段真实日志(G1 GC,JDK 11)下面是 G1 收集器的一段典型输出,我们逐段拆:[2026-07-09T10:15:32.1010800][3.204s] GC(12) Pause Young (Normal) (G1 Evacuation Pause) [2026-07-09T10:15:32.1010800][3.204s] GC(12) Eden regions: 24-0(24) [2026-07-09T10:15:32.1010800][3.204s] GC(12) Survivor regions: 3-3(4) [2026-07-09T10:15:32.1010800][3.204s] GC(12) Old regions: 10-12 [2026-07-09T10:15:32.1010800][3.204s] GC(12) Humongous regions: 0-0 [2026-07-09T10:15:32.1020800][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:formatb,fileheap.bin pid导出堆快照,用 MAT(Memory Analyzer)分析是谁在占内存不放。快速上手的排查命令不想翻日志时,几个命令能快速看现状:# 实时看各代内存和 GC 次数/耗时,每 1 秒刷一次jstat-gcutilpid1000# 输出里 YGC/YGCT 是 Young GC 次数/总耗时,FGC/FGCT 是 Full GC 次数/总耗时# 重点看 FGC 是不是在快速增长,O(老年代使用率%)是不是一直贴近 100# 看堆里对象数量和大小排名,快速定位可疑的大户jmap-histo:livepid|head-20jstat -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 频不频、回收后堆降不降、单次停顿长不长。