JVM知识梳理
2026/7/31 4:45:15 网站建设 项目流程

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) 如果还是放不下 → OutOfMemoryError

3.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);// true

4.5 常见异常

异常原因
OutOfMemoryError: Metaspace加载的类太多、元空间不足
# JDK 8+ 调整元空间大小-XX:MetaspaceSize=256m-XX:MaxMetaspaceSize=512m

五、其他区域

5.1 程序计数器(PC Register)

  • 作用:记录当前线程执行的字节码行号
  • 线程私有:每个线程独立一个
  • 唯一不会 OOM 的区域
线程 A 执行到第 10 行 → PC = 10 线程 B 执行到第 20 行 → PC = 20

5.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 堆外内存,不属于运行时数据区)

📝 写在最后

学习建议

  1. 堆栈方法区必须讲清楚:面试必问,能说清"栈存引用、堆存对象、方法区存类信息"就过关
  2. 分代结构要理解:不是死记硬背,理解"为什么分代"——新生代朝生夕死用复制算法,老生代存活率高用标记-整理
  3. JDK 8 方法区变化要知道:永久代 → 元空间,原因是永久代大小固定容易 OOM,元空间用本地内存更灵活
  4. 常见异常要对应:StackOverflow 对应栈,OOM 对应堆/方法区,面试会问"什么情况下会抛什么异常"

面试回答模板

Q:说说 JVM 运行时数据区?

JVM 运行时数据区分为线程私有和线程共享两部分。

线程私有包括程序计数器(记录字节码行号,唯一不会 OOM 的区域)、虚拟机栈(存储局部变量和方法调用,每次方法调用创建一个栈帧)、本地方法栈(为 Native 方法服务)。

线程共享包括堆(存储对象实例,分新生代和老生代,新生代用复制算法回收效率高,老生代用标记-整理)和方法区(JDK 8 之前用永久代,之后用元空间,存储类信息、常量、静态变量)。

重点说一下堆和栈的区别:栈存局部变量和引用,堆存对象实例;栈是线程私有的,堆是共享的;栈分配速度快但空间小,堆分配速度慢但空间大。

Q:为什么堆要分新生代和老生代?

分代是为了提高 GC 效率。大部分对象朝生夕死,放在新生代,用复制算法每次只复制少量存活对象,效率高;少量长期存活的对象进入老生代,用标记-清除或标记-整理算法,适合存活率高的场景。这样不同区域用不同回收策略,整体 GC 效率更高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询