Java 语言的“一次编写,到处运行”(Write Once, Run Anywhere)特性得以实现,核心在于 Java 虚拟机(JVM)。JVM 不仅是 Java 代码的执行引擎,更是一个精密的内存管理器。了解 JVM 如何组织和管理内存,是每一位 Java 开发者进阶的必经之路,也是进行性能调优和排查内存问题的基础。
本文将详细剖析 JVM 的运行时数据区(Runtime Data Areas),阐明其各个组成部分的功能和生命周期,并澄清其与 Java 内存模型(JMM)的区别。
首先,我们必须区分两个极易混淆的概念:
简而言之:运行时数据区是 JVM 的 “地盘划分”,规定了内存有哪些部分;而 JMM 是多线程通信的 “交通规则”,规定了线程如何安全地共享数据。
根据 JVM 规范,运行时数据区可分为两大类:线程共享区和线程私有区。
线程共享区的数据随虚拟机的启动而创建,随虚拟机的关闭而销毁。区域内的内容可以被所有线程访问,因此在并发访问时需要考虑线程安全问题。
堆是 JVM 管理内存中最大的一块区域,也是垃圾收集器(GC)管理的主要区域。
- 功能:存放对象实例和数组。Java 中几乎所有的对象实例都在这里分配内存。
- 特点:
- 所有线程共享。
- 大小可以动态扩展,通过
-Xms
(初始大小) 和 -Xmx
(最大大小) 参数设定。
- 如果堆中没有足够内存完成实例分配,并且堆也无法再扩展时,JVM 会抛出
java.lang.OutOfMemoryError: Java heap space
异常。
- 内部结构:为了优化 GC 效率,现代 JVM 的堆通常被划分为:
- 新生代 (Young Generation):存放新创建的对象。又细分为 Eden 区和两个 Survivor 区(S0 和 S1)。绝大多数对象在新生代被创建,并在 minor GC 中被回收。
- 老年代 (Old Generation):存放经过多次 minor GC 后仍然存活的对象,或者一些大对象。此区域的 GC(Major GC / Full GC)频率较低。
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。
- 功能:存储类的元数据。
- 别名与演进:
- 在 JDK 7 及以前,方法区通常由永久代 (Permanent Generation, PermGen) 实现。永久代属于堆的一部分,并且有固定的大小限制,容易导致
java.lang.OutOfMemoryError: PermGen space
。
- 自 JDK 8 起,永久代被彻底移除,取而代之的是元空间 (Metaspace)。元空间使用的是本地内存(Native Memory),而非 JVM 堆内存,从而解决了 PermGen 的大小限制问题(默认仅受限于系统可用内存)。
- 运行时常量池 (Run-Time Constant Pool):
- 它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 类加载后,这些内容会存放到运行时常量池中。
String
类的 intern()
方法就与运行时常量池有关。
线程私有区的生命周期与线程相同,随线程的创建而创建,随线程的销毁而销毁。每个线程都拥有自己独立的一份,因此不存在线程安全问题。
- 功能:每个方法在执行时,JVM 都会同步创建一个栈帧 (Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。我们常说的“栈内存”指的就是这里。
- 生命周期:一个方法的调用到执行完毕,就对应一个栈帧在虚拟机栈中的入栈到出栈过程。
- 特点:
- 以 LIFO(后进先出)的方式组织。
- 局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference 类型)。
- 异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,会抛出
java.lang.StackOverflowError
异常(例如,无限递归调用)。
- 如果虚拟机栈可以动态扩展,但在尝试扩展时无法申请到足够的内存,会抛出
java.lang.OutOfMemoryError
异常。
public class StackExample {
public void methodA() {
int a = 10; // 'a' 存放在 methodA 的栈帧的局部变量表中
methodB();
}
public void methodB() {
String b = "hello"; // 'b' 存放在 methodB 的栈帧的局部变量表中
}
}
// 调用 methodA 时:
// 1. methodA 的栈帧入栈。
// 2. methodA 调用 methodB,methodB 的栈帧入栈。
// 3. methodB 执行完毕,其栈帧出栈。
// 4. methodA 执行完毕,其栈帧出栈。
- 功能:可以看作是当前线程所执行的字节码的行号指示器。它记录了下一条需要执行的 JVM 指令的地址。
- 特点:
- 是所有内存区域中唯一一个在 JVM 规范中没有规定任何
OutOfMemoryError
情况的区域。
- 如果线程正在执行的是一个 Java 方法,PC 寄存器记录的是正在执行的虚拟机字节码指令的地址。
- 如果正在执行的是本地(Native)方法,则 PC 寄存器的值为空(Undefined)。
- 功能:与虚拟机栈类似,但它为虚拟机使用到的本地(Native)方法服务。
- 说明:当一个线程调用一个 C 或 C++ 等语言编写的本地方法时,它会进入一个不受 JVM 管理的内存区域,本地方法栈就是为这些调用服务的。具体的实现由虚拟机厂商决定,例如 HotSpot 虚拟机就将本地方法栈和虚拟机栈合二为一。
graph TD
subgraph JVM
subgraph Thread-Shared Areas
Heap["堆 (Heap)<br/>存放对象实例和数组<br/>(GC 主要管理区域)"]
MethodArea["方法区 (Method Area)<br/>JDK 8+ 为 Metaspace<br/>存放类信息、常量、静态变量"]
subgraph Heap
YoungGen["新生代<br/>(Eden, S0, S1)"]
OldGen["老年代"]
end
subgraph MethodArea
RTCP["运行时常量池"]
end
end
subgraph Thread-Private Areas
Thread1["线程 1"] -- owns --> S1["虚拟机栈 (Stack)"]
Thread1 -- owns --> PC1["程序计数器 (PC)"]
Thread1 -- owns --> NMS1["本地方法栈 (Native)"]
Thread2["线程 2"] -- owns --> S2["虚拟机栈 (Stack)"]
Thread2 -- owns --> PC2["程序计数器 (PC)"]
Thread2 -- owns --> NMS2["本地方法栈 (Native)"]
Note["... 更多线程 ..."]
end
end
style Heap fill:#f9f,stroke:#333,stroke-width:2px
style MethodArea fill:#ccf,stroke:#333,stroke-width:2px
style S1 fill:#9cf,stroke:#333,stroke-width:2px
style PC1 fill:#9fc,stroke:#333,stroke-width:2px
style NMS1 fill:#f99,stroke:#333,stroke-width:2px
style S2 fill:#9cf,stroke:#333,stroke-width:2px
style PC2 fill:#9fc,stroke:#333,stroke-width:2px
style NMS2 fill:#f99,stroke:#333,stroke-width:2px
理解 JVM 运行时数据区的划分是掌握 Java 程序运行机制的基石。
- 线程共享区(堆、方法区) 是数据存储和 GC 的核心地带,也是并发问题的源头。
- 线程私有区(虚拟机栈、本地方法栈、程序计数器) 保证了线程执行的独立性和正确性。