【JVM】JVM内存结构
【JVM】彻底搞懂JVM内存结构:对象、方法、变量都存在哪?
- 一、JVM内存的核心划分
- 二、线程私有
- 1. 虚拟机栈
- 2. 本地方法栈
- 3. 程序计数器
- 虚拟机栈、本地方法栈、程序计数器可以线程共享吗
- 三、线程共享
- 1. 堆
- 2. 方法区
- 为什么堆和方法区是线程共享
- 四、总结表
你有没有好奇过,Java 代码运行时,new出来的对象存在哪?方法执行时,方法本身存在哪?方法里的参数、局部变量又存在哪?一个类的元数据信息 —— 比如类的接口、父类、属性、函数,还有代码里的分支、循环、函数调用跳来跳去的,JVM 怎么知道代码执行到哪一行了?这些东西都是怎么存储的?要搞懂这些,就得先了解 JVM 内存结构。
一、JVM内存的核心划分
JVM 内存结构主要划分为这几个核心区域:虚拟机栈、本地方法栈、程序计数器、堆和方法区。

二、线程私有
线程私有区域指的是每个线程单独拥有一份,互不干扰,随线程创建而生成,随线程销毁而释放,不需要垃圾回收(GC)清理。
1. 虚拟机栈
虚拟机栈本质是一个栈结构(后进先出,LIFO),核心作用是管理方法的执行流程。我们调用任何一个方法时,JVM都会做一件事:创建一个「栈帧」(Stack Frame),并把它压入虚拟机栈;当方法执行完成(正常返回或抛出异常),这个栈帧就会被弹出栈。
比如执行 main() 方法时,先创建 main 栈帧压入栈;main 里调用 add() 方法,再创建 add 栈帧压入栈(此时 add 在栈顶);add 执行完弹出,回到 main 栈帧,直到 main 执行完弹出,程序结束——这就是方法执行的“后进先出”逻辑。
那方法里的局部变量、入参、出参这些东西存在哪?刚才说了,方法调用会产生栈帧,栈帧里的局部变量表就是用来保存局部变量和方法参数的;栈帧里还有操作数栈,专门用来做算术运算和方法调用的参数传递。栈帧里还有其他信息,但新手知道这两个核心部分就够了。

虚拟机栈需要 GC 吗?不需要!因为方法执行完就自动出栈,栈帧占用的内存也会跟着释放,全程自动管理,不用额外靠 GC 回收。
2. 本地方法栈
本地方法栈也是一个栈结构,所谓“本地方法”,就是用C/C++编写的方法(Java底层很多功能,比如IO操作、线程管理,都是调用本地方法实现的)。当Java代码调用 System.currentTimeMillis() 这种本地方法时,JVM会创建本地方法对应的栈帧,压入本地方法栈;方法执行完弹出,流程和虚拟机栈一样。
3. 程序计数器
程序计数器是JVM里最小的内存区域,核心作用是记录当前线程执行的字节码指令位置。
因为代码里有分支、循环、方法调用,执行时会从这个类跳到另一个类,从这一行跳到那一行,所以需要程序计数器来保存 “即将执行的字节码指令的位置”。
- 执行普通指令时,计数器记录“下一条要执行的字节码指令地址”;
- 调用方法时,计数器记录“当前方法的返回地址”;
- 多线程切换时,计数器会保存当前线程的执行位置——切换回该线程时,就能从上次的位置继续执行,不会乱序。
保存了位置之后,JVM 就知道下一步该执行哪行代码了。
虚拟机栈、本地方法栈、程序计数器可以线程共享吗
虚拟机栈可以线程共享吗?不行:不同线程可以并发执行,比如 A 线程调用 A 方法,B 线程调用 B 方法,肯定需要不同的虚拟机栈来保存各自正在执行的方法信息;本地方法栈也是同理。
而且不同线程执行的位置肯定不一样,执行到哪一行的进度也不同,所以必须每个线程单独用一个程序计数器记录自己的执行位置 —— 这些区域必须是线程私有的,各线程互不干扰。
三、线程共享
线程共享区域是JVM内存的“大头”,堆和方法区由所有线程共用,也是垃圾回收的主要战场(方法区在JDK1.8后有特殊处理)。
1. 堆
堆是线程共享的核心区域,是JVM中最大的内存区域,作用很明确:几乎所有new出来的对象都会存放在堆里。
那基本类型(比如int、char)放在哪?
- 方法内的基本类型变量(比如
int age = 20):存在虚拟机栈的「局部变量表」里; - 基本类型数组(比如
int[] arr = new int[3]):数组对象本身存在堆里,数组里的元素(arr[0])也存在堆里; - 对象的基本类型属性(比如
User类的private int age):随对象一起存在堆里。
为什么基本类型放栈、对象放堆?不能都放一起吗?
这是 JVM 基于性能和使用场景的设计:
- 大小差异:基本类型(比如int占4字节)体积小,放栈里分配和销毁速度快;对象(比如一个复杂的
Order对象)体积可能很大,放栈里会导致栈内存溢出; - 生命周期差异:基本类型随方法执行而存在,方法结束就销毁;对象可能需要跨方法、跨线程存在(比如一个
User对象被多个线程调用),堆的生命周期更长,更适合存储; - 共享需求:对象需要被多个线程共享,堆是共享区域,只需通过“引用”(栈里的变量指向堆里的对象)就能访问;如果对象放栈里(线程私有),共享时需要深拷贝,效率极低。
所有对象都会放到堆里吗?
其实不一定。有些情况下,JVM 会对对象进行 “栈上分配”:通过 “逃逸分析” 判断对象是否只在当前方法内部使用 (没有作为返回值返回、没有传给其他方法或外部对象),这种 “没有逃逸” 的对象,JVM 会把它拆成基本类型,直接在虚拟机栈上分配,不用占用堆内存。
比如下面的代码,User 对象只在 createUser() 里使用,没有逃逸,就可能被栈上分配:
public void createUser() {User user = new User(); // 没有返回,没有传给其他方法user.setName("张三");
}
2. 方法区
方法区里放的不是方法,而是类的元数据信息
- 类的基本信息:全类名(比如
com.test.User)、父类、实现的接口、访问修饰符; - 类的结构信息:字段(属性)的名称、类型、访问修饰符;方法的名称、参数列表、返回值类型、方法体字节码;
- 静态变量:被
static修饰的变量(比如public static String name = "Java"); - 常量池:存放字符串常量(比如
"hello")、符号引用(比如类名、方法名的引用)等。
方法区不同 JVM 的实现不一样:JDK1.8 之前,方法区的实现叫 “永久代”,是在JVM内存里面的,属于运行时数据区的一部分;JDK1.8 及之后,实现改成了 “元空间”,放到了本地内存(也就是操作系统的内存)里。

为什么要把永久代换成元空间,还放到本地内存?
永久代在JVM内存里,默认空间不大,当加载的类太多(比如SpringBoot项目依赖众多jar包),很容易触发 PermGen Space OOM;
而元空间依赖操作系统内存,加载多少类的元数据信息就由系统的实际可用空间来控制,能加载更多类,大幅降低 OOM 风险。
为什么堆和方法区是线程共享
堆和方法区都是线程共享的,原因也很简单:堆里的对象需要被不同线程访问;方法区的类元数据是创建对象的 “模板”,多个线程创建同一类的对象时,都要读取这里的元数据,所以必须共享。
四、总结表
| 区域 | 线程共享性 | 核心存储内容 | 生命周期 | 是否需要GC |
|---|---|---|---|---|
| 虚拟机栈 | 私有 | 栈帧(局部变量表、操作数栈等) | 随线程生灭 | 不需要 |
| 本地方法栈 | 私有 | 本地方法的栈帧 | 随线程生灭 | 不需要 |
| 程序计数器 | 私有 | 当前线程执行的字节码指令位置 | 随线程生灭 | 不需要 |
| 堆 | 共享 | new 出来的对象、数组 | 随对象生灭(可跨线程) | 需要 |
| 方法区(元空间) | 共享 | 类元数据、静态变量、常量池 | 随类加载/卸载生灭 | 部分需要 |
