JVM内存模型是调优GC、排查OOM和理解多线程可见性的底层依据;堆存对象并分年轻代(Eden/S0/S1)与老年代,栈存线程私有局部变量和引用,程序计数器记录字节码地址,Metaspace存类元数据。

JVM内存模型不是“一张图背下来就行”的概念,而是你调优GC、排查OOM、理解多线程可见性问题的底层依据——它直接决定你的对象存在哪、谁能看到、什么时候被回收。
堆(Heap):所有对象的“公共仓库”,也是GC主战场
你用 new 创建的对象、数组、包装类实例(如 Integer)、甚至字符串常量池中通过 new String("abc") 生成的对象,全在堆里。堆是线程共享的,所有线程都能通过引用访问同一块堆内存。
- 年轻代(Young Gen)占堆默认约1/3,分
Eden、S0、S1三块,比例通常是8:1:1;新对象优先分配在Eden; - 一次
Minor GC后还活着的对象,会从Eden进入S0;下次 GC 再挪到S1,来回倒腾; - 熬过几次 Minor GC(默认15次,由
-XX:MaxTenuringThreshold控制)或 Survivor 区放不下,就晋升到老年代(Old Gen); - 老年代满了触发
Full GC(也叫Major GC),停顿时间长,是性能瓶颈高发区。
⚠️ 容易踩的坑:ArrayList 扩容、String.substring()(JDK 7u6 之前)、大量缓存未清理,都可能让对象“意外滞留”在老年代,加速 Full GC。
栈(Java Virtual Machine Stack):每个线程独享的“执行快照”
方法一调用,JVM 就给它分配一个栈帧(Stack Frame);方法返回,栈帧弹出销毁。栈里只存三类东西:局部变量(含基本类型值、对象引用地址)、操作数栈(JVM 执行指令用的临时空间)、动态链接与出口信息(用于方法调用跳转)。
-
int x = 42;→ 值42直接压栈; -
String s = new String("hi");→ 引用地址存栈,"hi"对象本身在堆; - 成员变量(如
private int count;)属于对象的一部分,随对象一起在堆上分配,不在栈里; - 栈大小受限于
-Xss参数,默认一般 1MB;递归太深或局部变量巨多,会抛StackOverflowError。
程序计数器(PC Register)和方法区(Metaspace):小但关键的“元数据管家”
程序计数器 是唯一不会 OOM 的区域,每个线程私有,只记下一条要执行的字节码指令地址(本地方法则为 undefined)。它是线程切换后能“接着上次断点跑”的基础。
方法区 在 JDK 8+ 叫 Metaspace,存类结构、常量池、静态变量、JIT 编译后的代码。它不再受 -XX:PermSize 限制,而是直接使用本地内存;但若持续加载类(如热部署、OSGi、反射生成大量代理类),仍可能触发 OutOfMemoryError: Metaspace。
- 类的
static final String字面量进运行时常量池(在 Metaspace); - 而
static String s = new String("abc");中的"abc"对象在堆,引用变量s的地址也在 Metaspace 的类静态字段区; -
-XX:MaxMetaspaceSize必须设(否则可能吃光系统内存),建议初始值与最大值一致以避免动态扩容开销。
真正卡住人的从来不是“堆有几块”,而是当 OutOfMemoryError: Java heap space 报出来时,你得立刻判断:是缓存没清?是流没关?还是对象图里藏着不该活这么久的引用?——这些判断,全依赖你对堆、栈、Metaspace 各自职责和生命周期的肌肉记忆。









