JVM知识梳理
作者:没有四次元口袋的蓝胖
日期:2026-07-30
标签:Java, JVM, 运行时数据区, 堆, 栈, 方法区
JVM 内存模型是 Java 面试的必考题。重点掌握堆、栈、方法区三个核心区域,其他区域了解即可。
一、整体划分
JVM 运行时数据区分为线程私有和线程共享两部分:
JVM 运行时数据区 ├── 线程私有(每个线程独立,随线程创建/销毁) │ ├── 程序计数器(PC Register) │ ├── 虚拟机栈(VM Stack) │ └── 本地方法栈(Native Method Stack) ├── 线程共享(所有线程共用,JVM 启动时创建) │ ├── 堆(Heap) │ └── 方法区(Method Area) └── 直接内存(Direct Memory,NIO 使用,不属于 JVM 运行时数据区)面试关键区分:
- 线程私有 → 随线程生灭,不会有并发问题
- 线程共享 → 需要考虑线程安全和 GC
二、虚拟机栈(VM Stack)— 重点
2.1 作用
存储方法的局部变量表、操作数栈、动态链接、方法出口地址。
每次方法调用都会在栈中创建一个栈帧,方法执行完毕栈帧出栈。
┌─────────────────────────────┐ │ 方法调用 3 │ ← 栈顶(当前执行的方法) │ ┌─────────────────────┐ │ │ │ 栈帧(Frame) │ │ │ │ - 局部变量表 │ │ │ │ - 操作数栈 │ │ │ │ - 动态链接 │ │ │ │ - 返回地址 │ │ │ └─────────────────────┘ │ ├─────────────────────────────┤ │ 方法调用 2 │ ├─────────────────────────────┤ │ 方法调用 1 │ ← 栈底 └─────────────────────────────┘2.2 局部变量表
存储方法内的局部变量:
- 基本数据类型(int、long、float、double、boolean、char、short、byte)
- 对象引用(reference,指向堆中对象的指针)
- returnAddress 类型
publicvoidtest(){inta=10;// 局部变量表存 aintb=20;// 局部变量表存 bObjectobj=newObject();// 局部变量表存引用,对象在堆中}2.3 常见异常
| 异常 | 原因 |
|---|---|
| StackOverflowError | 栈深度超过限制(递归太深、死循环调用) |
| OutOfMemoryError | 动态扩展栈时无法申请到足够内存 |
2.4 面试高频问题
Q:栈和堆的区别?
| 维度 | 栈 | 堆 |
|---|---|---|
| 存储内容 | 局部变量、方法调用 | 对象实例、数组 |
| 线程私有 | 是 | 否(所有线程共享) |
| 生命周期 | 随方法调用 | 由 GC 管理 |
| 分配速度 | 快(指针移动) | 慢(需要查找空闲内存) |
| 大小 | 较小(默认 1MB) | 较大(默认物理内存的 1/4) |
三、堆(Heap)— 重点
3.1 作用
存储对象实例和数组,是 GC(垃圾回收)的主要区域。
Objectobj=newObject();// 引用在栈中,对象在堆中3.2 堆的分代结构
现代 JVM 把堆分为新生代和老生代:
堆(Heap) ├── 新生代(Young Generation)—— 占 1/3 │ ├── Eden 区(80%) │ ├── Survivor From(10%) │ └── Survivor To(10%) └── 老生代(Old Generation)—— 占 2/3对象生命周期:
新对象 → Eden 区 ↓(Eden 满,Minor GC) Survivor From/To(复制存活对象) ↓(年龄达到阈值,默认 15) 老生代 ↓(老生代满,Major GC / Full GC) 如果还是放不下 → OutOfMemoryError3.3 为什么分代?
- 新生代:对象朝生夕死,GC 频繁但每次回收量小(复制算法,效率高)
- 老生代:对象存活率高,GC 较少但每次回收量大(标记-清除/标记-整理)
面试回答模板:分代是为了提高 GC 效率。新生代用复制算法,每次只复制少量存活对象;老生代用标记-清除或标记-整理,适合长期存活的对象。
3.4 常见异常
| 异常 | 原因 |
|---|---|
| OutOfMemoryError | 堆内存不足(对象太多、内存泄漏) |
# JVM 参数调整堆大小-Xms512m# 初始堆大小-Xmx2048m# 最大堆大小四、方法区(Method Area)— 重点
4.1 作用
存储类信息、常量、静态变量、JIT 编译后的代码。
注意:方法区是规范定义的概念,不同 JVM 实现不同。HotSpot 用永久代(JDK 8 之前)或元空间(JDK 8+)实现。
4.2 JDK 8 前 vs JDK 8+
| 版本 | 实现方式 | 位置 |
|---|---|---|
| JDK 7 及之前 | 永久代(PermGen) | JVM 进程内存 |
| JDK 8+ | 元空间(Metaspace) | 本地内存(Native Memory) |
为什么要改?
- 永久代大小固定,容易 OOM
- 元空间使用本地内存,理论上不受 JVM 内存限制
- 方便 HotSpot 与 JRockit 融合(JRockit 没有永久代)
4.3 方法区存储内容
方法区(元空间) ├── 类信息(Class):类名、父类、接口、访问修饰符 ├── 字段信息(Field):字段名、类型、修饰符 ├── 方法信息(Method):方法名、参数、返回类型、字节码 ├── 常量池(Constant Pool):字面量、符号引用 ├── 静态变量(Static Variables) └── JIT 编译后的代码4.4 运行时常量池
方法区中有一个运行时常量池,是类加载后生成的。
Strings1="abc";// "abc" 在运行时常量池Strings2="abc";// 同一个引用System.out.println(s1==s2);// true4.5 常见异常
| 异常 | 原因 |
|---|---|
| OutOfMemoryError: Metaspace | 加载的类太多、元空间不足 |
# JDK 8+ 调整元空间大小-XX:MetaspaceSize=256m-XX:MaxMetaspaceSize=512m五、其他区域
5.1 程序计数器(PC Register)
- 作用:记录当前线程执行的字节码行号
- 线程私有:每个线程独立一个
- 唯一不会 OOM 的区域
线程 A 执行到第 10 行 → PC = 10 线程 B 执行到第 20 行 → PC = 205.2 本地方法栈(Native Method Stack)
- 作用:为 Native 方法服务(如 C/C++ 编写的方法)
- 线程私有
- HotSpot 把本地方法栈和虚拟机栈合二为一
5.3 直接内存(Direct Memory)
- 作用:NIO 引入,通过
DirectByteBuffer分配堆外内存 - 不属于 JVM 运行时数据区
- 避免 Java 堆和 Native 堆之间的数据复制,提高 I/O 性能
// 堆外内存ByteBufferbuffer=ByteBuffer.allocateDirect(1024);六、各区域对比总结
| 区域 | 线程 | 存储内容 | 异常 |
|---|---|---|---|
| 程序计数器 | 私有 | 字节码行号 | 唯一不会 OOM |
| 虚拟机栈 | 私有 | 局部变量、方法调用 | StackOverflow / OOM |
| 本地方法栈 | 私有 | Native 方法 | StackOverflow / OOM |
| 堆 | 共享 | 对象、数组 | OOM |
| 方法区 | 共享 | 类信息、常量、静态变量 | OOM |
| 直接内存 | — | NIO 堆外内存 | OOM |
🗺️ 思维导图速览
JVM 运行时数据区 ├── 线程私有 │ ├── 程序计数器(字节码行号,唯一不会 OOM) │ ├── 虚拟机栈(栈帧:局部变量表/操作数栈/动态链接/返回地址) │ └── 本地方法栈(Native 方法) ├── 线程共享 │ ├── 堆(对象实例,分新生代/老生代) │ │ ├── 新生代(Eden + Survivor,复制算法) │ │ └── 老生代(标记-清除/标记-整理) │ └── 方法区(JDK 8+ 用元空间,本地内存) │ ├── 类信息、字段、方法 │ ├── 运行时常量池 │ └── 静态变量、JIT 代码 └── 直接内存(NIO 堆外内存,不属于运行时数据区)📝 写在最后
学习建议
- 堆栈方法区必须讲清楚:面试必问,能说清"栈存引用、堆存对象、方法区存类信息"就过关
- 分代结构要理解:不是死记硬背,理解"为什么分代"——新生代朝生夕死用复制算法,老生代存活率高用标记-整理
- JDK 8 方法区变化要知道:永久代 → 元空间,原因是永久代大小固定容易 OOM,元空间用本地内存更灵活
- 常见异常要对应:StackOverflow 对应栈,OOM 对应堆/方法区,面试会问"什么情况下会抛什么异常"
面试回答模板
Q:说说 JVM 运行时数据区?
JVM 运行时数据区分为线程私有和线程共享两部分。
线程私有包括程序计数器(记录字节码行号,唯一不会 OOM 的区域)、虚拟机栈(存储局部变量和方法调用,每次方法调用创建一个栈帧)、本地方法栈(为 Native 方法服务)。
线程共享包括堆(存储对象实例,分新生代和老生代,新生代用复制算法回收效率高,老生代用标记-整理)和方法区(JDK 8 之前用永久代,之后用元空间,存储类信息、常量、静态变量)。
重点说一下堆和栈的区别:栈存局部变量和引用,堆存对象实例;栈是线程私有的,堆是共享的;栈分配速度快但空间小,堆分配速度慢但空间大。
Q:为什么堆要分新生代和老生代?
分代是为了提高 GC 效率。大部分对象朝生夕死,放在新生代,用复制算法每次只复制少量存活对象,效率高;少量长期存活的对象进入老生代,用标记-清除或标记-整理算法,适合存活率高的场景。这样不同区域用不同回收策略,整体 GC 效率更高。