运行时数据区
运行时数据区是Java程序执行过程中管理的内存区域
Java 运行时数据区组成(JVM 内存结构)
Java 虚拟机(JVM)的运行时数据区由以下核心部分组成:
线程私有:程序计数器、Java虚拟机栈、本地方法栈。
线程共享:方法区、堆。
一、程序计数器
程序计数器存储当前执行的字节码指令地址。
线程私有:每个线程都有独立的程序计数器。
多线程场景下程序计数器的工作流程:
常见问题解答
Q:为什么程序计数器不会内存溢出?
A:其存储的是单个指令地址(指针),非用户数据。指针长度固定(32位系统4字节/64位8字节),且线程结束时自动释放。
Q:PC 如何影响程序流程控制?
A:分支指令直接修改 PC 值实现跳转:
0: iload_1
1: ifeq 12 // 如果值为0,PC跳转到12
4: iinc 1, -1 // 否则继续执行
...
12: return // 跳转目标
Q:调试器如何实现断点功能?
A:通过修改 PC 值实现:
- 在目标指令处插入特殊断点指令
- 当 PC 指向断点时暂停执行
- 显示当前堆栈和变量状态
二、Java虚拟机栈
Java虚拟机栈描述的是Java方法执行过程中的线程内存模型,主要作用是管理Java方法的调用过程。在当前线程中每个方法被调用的时候,JVM会同步创建一个栈帧呀入到虚拟机栈。栈帧中存储着局部变量表、操作数栈、动态链接、方法返回地址等。
栈帧组成
1. 局部变量表
-
存储内容:
-
this(实例对象的地址)
-
方法参数
-
局部变量
-
基本类型数据(
int
,boolean
等) -
对象引用(reference)
-
-
容量单位:变量槽(Slot)
-
32位类型占1个Slot(
int
,float
,reference
) -
64位类型占2个Slot(
long
,double
)索引 类型 名称 大小 示例值 0 reference this 1槽 0x00a3b1 1 int a 1槽 10 2 double b 2槽 20.5 4 reference c 1槽 0x00c4d2 5 long d 2槽 100
-
-
内存复用:Slot在作用域结束后可被复用
public void demo(int param) {
int a = 10; // Slot 0: this | Slot 1: param | Slot 2: a
double b = 20.0; // Slot 3-4: b (占2个Slot)
String s = "hello"; // Slot 5: s
}
2. 操作数栈
-
作用:执行字节码指令的工作区
-
深度:编译期确定(写入方法表的
max_stack
属性) -
示例代码及执行过程
以下面的 Java 代码为例,分析操作数栈在方法执行过程中的具体工作情况。
public class OperandStackExample { public static int add(int a, int b) { int c = a + b; return c; } public static void main(String[] args) { int result = add(3, 5); System.out.println(result); } }
编译后的字节码分析
add
方法编译后的部分字节码如下:public static int add(int, int); Code: 0: iload_0 // 将局部变量表中第 0 个位置的 int 型变量(参数 a)压入操作数栈 1: iload_1 // 将局部变量表中第 1 个位置的 int 型变量(参数 b)压入操作数栈 2: iadd // 从操作数栈中弹出两个 int 型操作数,相加后将结果压入操作数栈 3: istore_2 // 将操作数栈顶的 int 型结果弹出,存入局部变量表中第 2 个位置(变量 c) 4: iload_2 // 将局部变量表中第 2 个位置的 int 型变量(变量 c)压入操作数栈 5: ireturn // 将操作数栈顶的 int 型结果返回
操作数栈状态变化
- 执行
iload_0
指令后:将参数a
(值为 3)压入操作数栈,此时操作数栈栈顶元素为 3。 - 执行
iload_1
指令后:将参数b
(值为 5)压入操作数栈,此时操作数栈从栈顶到栈底元素依次为 5、3。 - 执行
iadd
指令后:从操作数栈中弹出 5 和 3,计算 3 + 5 = 8,将结果 8 压入操作数栈,此时操作数栈栈顶元素为 8。 - 执行
istore_2
指令后:将操作数栈顶的 8 弹出,存入局部变量表中变量c
的位置,此时操作数栈为空。 - 执行
iload_2
指令后:将局部变量表中变量c
的值 8 压入操作数栈,此时操作数栈栈顶元素为 8。 - 执行
ireturn
指令后:将操作数栈顶的 8 返回,方法执行结束。
- 执行
3. 动态链接
- 存储内容:指向方法区运行时常量池的引用
- 核心作用:将符号引用解析为直接引用
- 类方法调用:确定目标方法的入口地址
- 字段访问:定位字段在内存中的偏移量
4. 方法返回地址
- 两种返回方式:
- 正常返回:PC计数器值作为返回地址
- 异常退出:异常处理器表记录的地址
- 关键动作:
- 恢复上层方法的局部变量表
- 将操作数栈结果压回调用者栈帧
- 调整PC计数器
三、本地方法栈
本地方法栈是 JVM 为执行本地方法(Native Method)提供的内存区域。本地方法是使用非 Java 语言(如 C、C++)实现的方法,主要作用是管理本地方法的调用过程。
栈帧结构
本地方法栈的栈帧结构和 Java 虚拟机栈的栈帧类似,通常包含以下部分:
局部变量表:用于存储本地方法执行过程中的局部变量,包括基本数据类型和对象引用。
操作数栈:在本地方法执行计算时,用于存储操作数和中间结果。
动态链接:将符号引用转换为直接引用,以便在运行时能够正确调用方法。
方法返回地址:记录方法执行完毕后返回的位置。
以上都是线程不共享,每个线程私有的内存区域,与每个线程当前执行位置与内存模型息息相关,都在线程创建时创建,在线程销毁时销毁。
四、方法区
方法区存放着已经被虚拟机加载的类型信息、常量、静态变量等数据。
储存内容
-
类型信息:
- 类的全限定名(如
java.lang.String
) - 类的直接父类的全限定名(对于
Object
,没有父类) - 类的修饰符(
public
,abstract
,final
等) - 实现的接口列表
- 字段信息(字段名称、类型、修饰符)
- 方法信息(方法名称、返回类型、参数类型/数量、修饰符、字节码、操作数栈和局部变量表大小)
- 类的全限定名(如
-
运行时常量池:
- 是字节码文件中常量池的运行时表现形式,被每个类或接口所独有。
- 包含:
- 编译期已知的字面量:文本字符串、
final
常量值。 - 符号引用:类和接口的完全限定名、字段的名称和描述符、方法的名称和描述符。这些符号引用在类加载的解析阶段会被转化为直接引用(如内存地址)。
- 编译期已知的字面量:文本字符串、
JDK 7 之后,字符串常量池从方法区移到了堆中,但其他类型的常量(如整数常量、浮点常量等)依然存于运行时常量池,位于元空间。
-
静态变量:
- 静态变量存储在方法区,可供所有的实例访问。
实现演变
- 永久代(JDK7及以前):方法区使用永久代实现,永久代使用堆中的一部分内存,有固定大小限制,容易发生内存溢出。
- 元空间(JDK8以后):移除永久代,使用元空间实现方法区,元空间使用系统直接内存,不再受堆内存的限制。
五、堆
在JVM中,堆的作用是存放所有的对象实例。堆被所有的线程共享。
存储内容
1.对象实例
- 所有
new
创建的对象。 - 数组对象:
int[]
,double[]
等基本类型数组(数组对象本身及其元素值都在堆中连续空间中)。String[]
,Object[]
等引用类型数组(数组对象本身在堆中,其元素是指向堆中其他对象的引用)。
易混淆点
- 字符串对象与字符串常量池:
- 字符串对象本身(如
new String("abc")
或运行时拼接生成的String
)存储在堆中。 - 字符串常量池自 JDK 7 起移至堆中,它存储的是:
- 字符串字面量(如
"abc"
)的引用(指向堆中的String
对象)。 String.intern()
方法返回的字符串的引用。
- 字符串字面量(如
- 总结:字符串对象在堆,常量池(
StringTable
)也在堆(存储引用),但常量池本身是一个哈希表结构。
- 字符串对象本身(如
- 静态变量引用的对象:
- 静态变量(
static
修饰)的引用本身存储在方法区(JDK 7+ 的元空间)。 - 但静态变量指向的对象实例(如
static Object obj = new Object();
中的new Object()
)存储在堆中。
- 静态变量(
- **类信息:
- 类的元数据(如类名、方法字节码、字段结构等)存储在方法区(元空间),不在堆中。
- 类的
Class
对象(如String.class
)是一个特殊的对象实例,存储在堆中。
- 基本类型局部变量 vs. 成员变量:
- 成员变量(在对象内部):基本类型(如
int
,double
)的值直接存在堆内对象内存中。 - 局部变量(在方法内部):基本类型(如
int i = 10;
)的值存储在 Java 栈的栈帧的局部变量表 中,不在堆中。
- 成员变量(在对象内部):基本类型(如