JVM 内存模型与 GC 工作区:你到底该关注哪块地盘?

很多人学 GC,上来就记一堆算法、参数。但我觉得,第一步应该先搞清楚——GC 到底在哪块区域干活?你连地图都没看明白,怎么优化?

JVM 把运行时内存划分成五个区域:堆、方法区、程序计数器、虚拟机栈、本地方法栈。但 GC 只盯着一块半区域猛干——堆是主战场,方法区偶尔也参与。其他区域?基本不关 GC 的事。

这章我们就来把这五块地的边界、用途、GC 参与度彻底捋一遍。我会结合我自己的踩坑经历,告诉你到底哪里容易出问题。

1. 堆(Heap)—— GC 的主战场,没有之一

堆是 Java 对象的老巢。new 出来的对象,数组,全堆在这里。GC 的绝大多数工作就是在这块区域里来回扫荡。

堆的核心特点:
  • 线程共享,所有线程都能访问
  • GC 主战场(Young Gen → Old Gen → 长期存活)
  • 大小由 -Xms / -Xmx 控制
  • 内部又分为新生代(Eden、S0、S1)和老年代

我记得有一次做个微服务启动优化,发现 Full GC 特别频繁。仔细一看,老年代涨得飞快——原来是某个缓存对象一直在堆里赖着不走。嗯,堆的分配和回收策略,直接影响启动速度。

2. 方法区(Method Area)—— 元数据仓库,GC 的「编外人员」

方法区存的是类信息、常量、静态变量、JIT 编译后的代码。在 JDK 8 之后,它被元空间(Metaspace)取代,不再使用堆内存,而是直接使用本地内存。

GC 在方法区也会干活,但频率低很多。主要是卸载类和清理常量池中的废弃常量。不过说实话,我很少在项目里看到方法区因为 GC 出问题的,除非你特别折腾自定义类加载器。

个人习惯:我一般把方法区(元空间)的初始大小设得宽松一点,避免启动时频繁触发元空间扩容导致停顿。比如 -XX:MetaspaceSize=256m。

3. 程序计数器(Program Counter Register)—— 小透明,GC 完全不管

程序计数器是线程私有的,记录当前线程执行到哪条字节码指令。它非常小,几乎不消耗内存,而且 GC 压根不看它。你想想,它就是个指向下一条指令的指针,不存在内存回收的问题。

4. 虚拟机栈(JVM Stack)—— 存储栈帧,GC 不直接动它

每个线程都有自己的虚拟机栈,里面装着一堆栈帧(Stack Frame)。每个方法调用都会压入一个栈帧,方法结束就弹出。局部变量表、操作数栈、动态链接、方法出口都在这里面。

GC 不会直接回收栈内存。为什么?因为栈帧随着方法调用自然弹出,是自动管理的。但注意一点:栈里的引用变量指向堆里的对象,如果栈帧被弹出,堆对象就没有强引用了——这时候 GC 才会把它干掉。所以栈只是间接配合了 GC。

我曾经遇到过:一个线上故障,线程栈溢出了。原因是递归调用太深,每个栈帧都持有大量引用,虽然没有直接触发 GC,但导致堆里的对象迟迟无法被回收。说白了,栈的深度直接影响堆的存活判定。

5. 本地方法栈(Native Method Stack)—— 与 GC 几乎绝缘

本地方法栈跟虚拟机栈结构类似,区别是它为 native 方法服务。平时我们写的 Java 代码很少直接调 native,除非你用 JNI。这块区域 GC 基本不管。

GC 到底工作在哪些区域?一张表看清

内存区域 线程共享 GC 作用 典型异常 重点优化方向
堆 是 主要区域 OutOfMemoryError 对象分配、GC 停顿
方法区(元空间) 是 次要区域 OutOfMemoryError 类卸载、常量池回收
程序计数器 否 无 无(唯一没有 OOM 的区域) 无
虚拟机栈 否 间接影响 StackOverflowError 栈深度、局部变量引用
本地方法栈 否 无 StackOverflowError 极少关注

SVG 结构图:JVM 运行时内存布局与 GC 工作区

JVM 运行时内存布局与 GC 工作区 堆(Heap)—— GC 主战场 新生代(Eden + Survivor) 老年代(Old Generation) 方法区(元空间) 类信息 / 常量池 / 静态变量 GC 次要区域 虚拟机栈 (线程私有) 无直接 GC 程序计数器 无 GC 本地方法栈 无 GC GC 集中作用区域 堆:主要 方法区:次要 其他区域:GC 不直接参与 栈帧自动管理,无回收

实际案例:启动优化时我踩的一个坑

我记得有个启动优化项目,应用启动要加载一堆 Spring Bean 和缓存。我上来就调 -Xms 和 -Xmx,但 Full GC 还是频繁触发。后来用 -XX:+PrintGCDetails 一查,发现老年代里有一堆类元数据——是我启动时加载的第三方库反射生成的类。

这就是方法区(元空间)在捣乱。虽然堆没满,但元空间满了也会触发 Full GC。我这才明白,GC 的工作区不只是堆,方法区也得照顾到。

避坑指南:如果你遇到启动时频繁 Full GC,但堆内存还挺富裕,先去检查元空间(Metaspace)的使用情况。用 jstat -gc <pid> 看 MC 和 MU 两列,说不定能发现问题。

最后总结一下——GC 的核心工作区就是堆和方法区。程序计数器、虚拟机栈、本地方法栈都是线程私有的自动管理地带,GC 基本不碰。你优化启动速度,重点关注堆的分代配置和元空间大小就对了。其他的,偶尔看看栈深度别溢出就行。