在 Java 面试中,JVM 内存结构几乎是必问知识点。很多人能背出程序计数器、虚拟机栈、本地方法栈、堆和方法区这五个区域,但当面试官追问它们的作用、线程共享关系以及对象为什么在堆上时,往往就难以解释清楚。
JVM 运行时数据区域也叫内存布局,但需要注意它和 Java 内存模型(Java Memory Model,简称 JMM)完全不同,属于两个不同的概念。它主要由以下 5 大部分组成:
1. 程序计数器(线程私有)
程序计数器的作用很简单:记录当前线程执行的行号。它是一块较小的内存空间,可视为当前线程所执行字节码的行号指示器。
如果当前线程正在执行的是 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值为 undefined。值得注意的是,程序计数器内存区域是 JVM 规范中没有规定任何 OOM 情况的区域。
在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的。因此,在任一具体时刻,一个 CPU 内核只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,且不能互相干扰。
我们通过一段简单代码及字节码指令来看看程序计数器的实际工作过程。
public class PcDemo {
public static int add(int a, int b) {
return a + b;
}
}
对应的字节码指令大致如下:
0: iload_0 // 从局部变量表中加载变量 a 到操作数栈
1: iload_1 // 从局部变量表中加载变量 b 到操作数栈
2: iadd // 两数相加
3: ireturn // 返回
当方法开始执行时,PC 计数器设置为 0,指向第一条指令 iload_0。随着指令执行,计数器会更新指向下一条。例如执行完 iadd 后,PC 更新为 3,指向 ireturn。如果此时 JVM 发生线程切换,线程 A 的 PC 可能停在 2,线程 B 的 PC 停在 4,确保各自恢复执行时不会乱套。
2. Java 虚拟机栈(线程私有)
Java 虚拟机栈(JVM Stack)是线程私有的运行时数据区,生命周期与线程相同。当一个方法被调用时,JVM 会为该方法创建一个栈帧(Stack Frame)并压入虚拟机栈中。通常我们所说的'栈内存',指的就是这里。
栈帧中主要包含局部变量表、操作数栈、动态链接和方法返回地址等信息。当方法执行结束时,对应的栈帧会被弹出。
- 局部变量表:用于存储方法参数和内部定义的局部变量(包括基本类型和对象引用)。注意,对象引用存储的是对象在堆中的地址,而非对象本身。局部变量表的大小在编译期确定,执行期间不变,以 slot(槽位)形式存放。
- 操作数栈:JVM 在执行字节码指令时,用于临时存放参与运算的数据和保存计算结果的栈结构。可以理解为 JVM 执行计算时的'工作台'。
- 动态链接:把常量池中的符号引用解析为实际方法或字段引用的过程。就像查通讯录,先通过名字找到真实号码再拨打电话。
- 方法返回地址:方法执行结束后,程序需要返回到调用者继续执行的位置。直白地说,就是'这个方法执行完之后,下一条要执行的字节码在哪里'。
3. 本地方法栈(线程私有)
本地方法栈与 Java 虚拟机栈类似,只不过前者为虚拟机使用到的 Native 方法服务,后者为 Java 方法服务。两者机制相似,不再赘述。
4. 堆(线程共享)
堆(Heap)是 JVM 中用于存放对象实例和数组的运行时内存区域,所有线程共享。当代码创建对象时,本质上就是在堆里申请一块内存。
User user ();
线程栈 (引用地址)
堆 对象实例


