JVM 虚拟机栈:方法执行的内存模型
虚拟机栈(Java Virtual Machine Stack)是 JVM 运行时数据区的核心组件之一,它专门用于描述 Java 方法的执行过程。作为线程私有的内存区域,虚拟机栈的生命周期与线程一致,内部通过 “栈帧” 记录方法调用的状态,是理解方法执行、局部变量存储、操作数计算的关键。
虚拟机栈的基本特性
1. 线程私有
每个线程在创建时会同步创建一个虚拟机栈,各线程的虚拟机栈相互隔离,确保方法执行的线程安全性。例如,线程 A 调用 methodA()
和线程 B 调用 methodA()
会在各自的虚拟机栈中生成独立的栈帧,互不干扰。
2. 栈帧为核心单位
虚拟机栈的内部存储单元是栈帧(Stack Frame),每个方法的调用对应一个栈帧的 “入栈”,方法执行完成(正常返回或抛出异常)对应栈帧的 “出栈”。因此,虚拟机栈的操作极其简单:只有入栈(push
)和出栈(pop
)两种操作。
3. 无垃圾回收
虚拟机栈的内存随方法调用动态分配,随方法结束自动释放(栈帧出栈),无需 GC 介入,内存管理高效且确定。
4. 访问速度快
虚拟机栈的内存分配和释放基于连续的栈空间,访问速度仅次于程序计数器,远快于堆内存。
栈帧:方法执行的状态载体
栈帧是虚拟机栈的基本组成单元,每个栈帧对应一个方法的执行状态,内部包含局部变量表、操作数栈、动态链接、方法返回地址和附加信息五部分。
1. 局部变量表:存储方法参数与局部变量
局部变量表(Local Variable Table)是栈帧中用于存储方法参数和局部变量的内存区域,其结构为数字数组,容量在编译期已确定(记录在方法的 Code
属性的 locals
字段中),运行时不可动态调整。
(1)存储单元:变量槽(Slot)
局部变量表以 “变量槽(Slot)” 为基本存储单元,每个 Slot 占用 4 字节(32 位):
- 32 位类型(
byte
、short
、char
、int
、float
、对象引用、boolean
)占用 1 个 Slot; - 64 位类型(
long
、double
)占用 2 个连续的 Slot(遵循 “对齐” 原则,不允许跨方法拆分)。
(2)存储内容
- 方法参数:按参数声明顺序存储,实例方法的第一个 Slot 固定存储
this
引用(静态方法无this
); - 局部变量:方法内定义的变量,按定义顺序依次分配 Slot(已分配的 Slot 可被复用,如作用域结束的变量 Slot 可被后续变量使用)。
(3)实例解析
通过字节码查看局部变量表结构(使用 javap -v 类名
命令):
1 | // 示例方法 |
编译后的字节码片段(局部变量表相关部分):
1 | public void test(int, java.lang.String); |
- 实例方法的
this
固定占用 Slot 0; - 参数
a
(int)和b
(引用)分别占用 Slot 1 和 2; - 局部变量
c
(int)占用 Slot 3,总容量为 4 个 Slot(locals=4
)。
2. 操作数栈:方法执行的计算引擎
操作数栈(Operand Stack)是栈帧中用于临时存储计算中间结果、传递方法参数的内存区域,本质是一个后进先出(LIFO)的栈结构。
(1)栈深度与类型
操作数栈的最大深度(栈容量)在编译期确定(记录在方法 Code
属性的 stack
字段中):
- 32 位类型占用 1 个栈单位深度;
- 64 位类型占用 2 个栈单位深度。
(2)核心作用
- 中间结果存储:算术运算(如
i + j
)时,先将i
和j
入栈,执行加法指令后弹出结果并压栈; - 方法参数传递:调用方法时,将参数依次压入操作数栈,供被调用方法读取;
- 临时变量空间:替代寄存器暂存数据,减少对物理寄存器的依赖。
(3)实例解析
通过字节码查看操作数栈的工作过程:
1 | // 示例方法:计算 a + b |
编译后的字节码片段:
1 | public int add(int, int); |
过程解析:
iload_1
:从局部变量表 Slot 1 加载a
到操作数栈;iload_2
:从局部变量表 Slot 2 加载b
到操作数栈;iadd
:弹出a
和b
,计算和后将结果压栈;ireturn
:弹出结果作为返回值,栈帧出栈。
3. 动态链接:符号引用到直接引用的转换
动态链接(Dynamic Linking)是栈帧中用于支持方法调用的关键机制,其核心是将常量池中的 “符号引用” 转换为内存中的 “直接引用”(如方法的内存地址)。
(1)符号引用与直接引用
- 符号引用:编译期生成的字符串标识(如类名、方法名),存储在
class
文件的常量池中(如#3
、#8
等),与内存地址无关; - 直接引用:直接指向目标的内存地址(如指针、偏移量),可直接访问目标。
(2)转换时机
- 静态解析:类加载阶段(解析阶段)将部分符号引用(如静态方法、私有方法,无法被重写)转换为直接引用,运行期不再改变;
- 动态链接:运行期每次调用方法时转换符号引用(如接口方法、重写的子类方法),支持 “动态绑定”(晚期绑定)。
(3)实例解析
字节码中方法调用的符号引用:
1 | // 调用 System.out.println("hello") 的字节码 |
#3
、#7
、#8
是常量池中的符号引用,运行时通过动态链接转换为System.out
字段的内存地址和println
方法的入口地址。
4. 方法返回地址:恢复调用者执行状态
方法返回地址用于记录调用当前方法的上层方法的执行位置(即调用者的程序计数器值),确保方法执行完成后,调用者能从正确的位置继续执行。
(1)两种返回方式
- 正常返回(return):方法执行
return
指令时,将返回值压入调用者的操作数栈,然后将返回地址设置为调用者的程序计数器值,当前栈帧出栈; - 异常返回:方法执行过程中抛出未捕获的异常,此时无返回值,通过异常表找到调用者的异常处理入口,当前栈帧直接出栈。
5. 附加信息
栈帧中还可能包含与虚拟机实现相关的附加信息(如调试信息),这部分不是 JVM 规范强制要求的,不同虚拟机(如 HotSpot、J9)可能有不同实现。
虚拟机栈的异常
虚拟机栈的内存大小可通过参数配置(固定大小或动态扩展),可能出现两种异常:
1. StackOverflowError
当线程请求的栈深度超过虚拟机允许的最大深度时抛出,典型场景是递归调用无终止条件。例如:
1 | public void recursive() { |
可通过 -Xss
参数调整栈大小(如 -Xss1m
表示栈大小为 1MB),但栈大小过大会导致可创建的线程数减少(总内存固定时)。
2. OutOfMemoryError
- 若虚拟机栈支持动态扩展,当扩展时无法申请到足够内存时抛出;
- 若虚拟机栈为固定大小,当创建线程时无法为其分配初始栈内存时抛出(如系统内存不足,无法创建新线程)
v1.3.10