JVM 运行时数据区域,通常也被称为内存布局,是理解 Java 并发与性能优化的基石。需要注意的是,它与 Java 内存模型(JMM)是两个完全不同的概念。该区域主要由以下五大部分组成:

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。 - 执行第一条指令:将局部变量表中索引为 0 的整数(即参数 a)加载到操作数栈顶。完成后 PC 更新为 1。
- 执行第二条指令:将索引为 1 的整数(即参数 b)加载到操作数栈顶。完成后 PC 更新为 2。
- 执行第三条指令:弹出栈顶两个整数相加,结果压入栈顶。完成后 PC 更新为 3。
- 执行最后一条指令:弹出结果作为返回值,方法结束。
若发生线程切换,例如线程 A 和线程 B 同时运行:
- 线程 A 的 PC = 2(下一条应执行
iadd) - 线程 B 的 PC = 4(假设其已执行完,或处于不同状态)
这确保了每个线程都能独立维护自己的执行上下文。
2. Java 虚拟机栈(线程私有)
Java 虚拟机栈(JVM Stack)是线程私有的运行时数据区,生命周期与线程相同。当一个方法被调用时,JVM 会为该方法创建一个栈帧(Stack Frame)并压入虚拟机栈中。栈帧主要包含局部变量表、操作数栈、动态链接和方法返回地址等信息。当方法执行结束时,对应的栈帧会被弹出。

栈帧内部的具体构成包括:
- 局部变量表(Local Variable Table):存储方法参数和局部变量(基本类型和对象引用)。对象引用存储的是堆中对象的地址而非对象本身。局部变量表大小在编译期确定,以 slot(槽位)形式存放。
- 操作数栈:用于临时存放参与运算的数据和保存计算结果的栈结构。字节码指令通过入栈(push)和出栈(pop)完成计算,相当于 JVM 执行计算的'工作台'。
- 动态链接:将常量池中的符号引用解析为实际方法或字段引用的过程。好比通讯录中通过名字查找电话号码,名字是符号引用,号码是真实引用。
- 方法返回地址:方法执行结束后,程序需要返回到调用者继续执行的位置。简单来说,就是'这个方法执行完后,下一条要执行的字节码在哪里'。
3. 本地方法栈(线程私有)
本地方法栈(Native Method Stack)与 Java 虚拟机栈类似,区别在于前者为虚拟机使用的 Native 方法服务,后者则为 Java 方法服务。其运行机制与虚拟机栈基本一致,此处不再赘述。
4. 堆(线程共享)
堆(Heap)是 JVM 中用于存放对象实例和数组的运行时内存区域,所有线程共享。当代码创建对象时,本质上就是在堆里申请一块内存。
User user = new User();
// 线程栈:user (引用地址)
// 堆:-------> User 对象实例
// 栈里只保存引用(reference),真正的数据在堆里。这也是为什么 Java 对象通常说是'在堆上分配'。
为什么堆必须共享? 设想这样一个场景:
User user = new User();
ThreadA 修改 user.name
ThreadB 读取 user.name
如果堆不是共享的,每个线程都有自己的对象副本,数据就会不同步。因此 JVM 的设计非常明确:对象数据在堆里共享,执行过程在栈里隔离。
常见的 JVM 参数 -Xms10m(最小启动内存)和 -Xmx10m(最大运行内存)都是针对堆的设置。
堆也是垃圾收集器管理的主要区域,因此被称为 GC 堆。基于分代收集算法,堆细分为新生代和老年代。新生代又包含 Eden 空间、From Survivor、To Survivor 等。进一步划分旨在更好地回收内存或更快分配内存。
堆最容易出现的错误是 OutOfMemoryError:
OutOfMemoryError: GC Overhead Limit Exceeded:JVM 花太多时间执行垃圾回收却只能回收很少的堆空间。java.lang.OutOfMemoryError: Java heap space:创建新对象时,堆内存不足以存放。这与物理内存无关,主要取决于配置的虚拟机内存大小。
5. 方法区(线程共享)
方法区是 JVM 规范上的逻辑区域,在不同 JDK 版本实现不同。JDK 7 时称为永久代(PermGen),JDK 8 起被元空间(Metaspace)取代。
注意:方法区 ≠ 元空间。方法区是规范里的概念,元空间是一种实现方式。
方法区主要存储什么?
- 类的结构信息:JVM 加载类时,会将类名、父类、接口、字段、方法、访问修饰符等元数据存储在此。
- 运行时常量池:Class 文件中的常量池,包含字符串常量、类名、方法名、字段名及符号引用。例如
String s = "hello";中的 "hello" 引用会进入运行时常量池。 - 静态变量(static 变量):类级别变量,不随对象销毁而销毁,存放在方法区。
- JIT 编译后的代码:热点方法被即时编译器(JIT)编译成的本地机器码也存储在此区域。
理解了这些区域的职责与交互机制,对于排查内存溢出问题及优化 JVM 参数配置至关重要。


