JVM规范之栈帧
- 前言
- 正文
- 概述
- 局部变量表
- 操作数栈
- 动态链接
- 总结
- 参考链接
前言
上一篇文章了解了JVM规范中的运行时数据区:
JVM规范之运行时数据区域
其中,栈是JVM线程私有的内存区,栈中存储的单位是帧(frames),本篇文章通过JVM8规范学习栈帧在JVM运行时的作用。
正文
概述
每个栈帧的内存分配自线程私有的 Java 虚拟机栈(JVM Stack),线程调用方法时压栈,方法结束时出栈销毁。
帧的作用是保存方法调用的局部变量、中间结果、执行动态链接、方法返回信息、发送异常。一个帧通常包含局部变量表、操作数栈、运行时常量池引用,这些组件都是帧私有的。
创建/销毁时机:
当方法被调用的时候,栈帧也会被创建,当方法调用完成时,栈帧被销毁,方法调用的含义包含两种:
- 方法正常调用结束,方法运行期间,JVM 没有抛出或者没有显式地使用throw抛出未被捕获的异常;
- 方法非正常结束(如抛出未捕获异常)时,栈帧会提前销毁,JVM 通过异常栈轨迹(StackTrace)记录各层栈帧信息,用于调试;
内存分配策略:
局部变量表和操作数栈的大小在编译时确定,运行时常量池引用的大小本质是一个指针,具体大小可能取决于具体的 JVM 实现,因此栈帧的大小由编译时确定的局部变量表和操作数栈的理论最大值,结合 JVM 实现的内存布局(如 Slot 字节数、对齐策略)决定。
局部变量表
- 每个栈帧都有自己的局部变量表,局部变量表的大小是在编译时确定的,因此在运行时可以一次性从栈上进行分配;
- 局部变量表通过索引的方式访问,每个索引的位置可以理解一个变量槽(slot),每个槽可以存储
boolean
,byte
,char
,short
,int
,float
,reference
,returnAddress
类型的值,一对slot可以存储long
和double
类型的值; long
和double
占用两个连续的变量槽,比如一个long
占用了索引n
和n+1
两个变量槽的位置,但是n+1
位置是不可读取的,可以被写入,这会导致变量槽n
位置的数据失效;- JVM 规范没有限制局部变量表中的数据必须进行字节对齐;
- 在调用方法时,JVM 使用局部变量表传递参数,从索引位置
0
开始,如果调用的方法是一个实例方法,索引0
位置总是被传入this
;对于静态方法,局部变量表索引0
不存储this
,直接从索引0
开始存储方法参数。
操作数栈
- 每个栈帧都包含一个操作数栈,栈的最大深度是编译时确定的,当栈帧被创建时,操作数栈是空的;
- 操作数栈符合栈的特点,LIFO,操作数栈中每个entry可以容纳一个JVM数据类型,包含
long
和double
类型; - 操作数栈中存储的数据类型和操作指令必须严格匹配,JVM会在进行class文件验证时,检查操作数栈的使用是否符合规范;
- 操作数栈中,
long
/double
作为 64 位值,占用 2 个深度单位,其他类型占 1 个;
动态链接
什么是动态链接?
在 JVM 中,动态链接是类加载机制和运行时环境的关键环节,主要用于将符号引用(Symbolic References)解析为直接引用(Direct References),是实现多态的核心技术。
- 每个栈帧都包含一个运行时常量池的引用,用于实现方法调用的动态链接过程;
- 字节码文件中描述了方法调用的符号表引用,但是并没有实际代码的内存地址,因此动态链接是将符号表引用翻译为直接引用;
- 动态链接是实现多态的核心机制,通过运行时常量池解析虚方法的符号引用,在运行时根据对象实际类型(如instanceof)找到具体方法的直接引用。例如,子类重写父类方法时,编译期符号引用指向父类,运行时动态链接到子类实现。
补充:
- 在编译阶段,字节码文件中仅保存符号表引用,不会涉及静态链接或者动态链接的过程;
- 静态链接在类加载阶段(解析阶段)确定方法调用目标,将符号表引用转化为直接引用,而动态链接指JVM在运行时才能确定调用目标的实例类型,在运行时将符号表引用转化为直接引用。
动态链接代码示例:
public class Animal {public void sound() {System.out.println("Animal"); } // 虚方法
}
public class Dog extends Animal {@Overridepublic void sound() {System.out.println("Woof");} // 动态链接目标
}
// 调用时通过运行时常量池解析为Dog.sound()
Animal animal = new Dog();
animal.sound(); // invokevirtual指令触发动态链接
总结
本篇文章根据JVM 8规范了解了栈帧的数据结构:
- 栈帧是在JVM线程进行方法调用时创建,每调用一个方法就会创建一个栈帧,每当方法调用结束或者异常结束,栈帧会被出栈;
- 每个栈帧通常包含局部变量表、操作数栈、运行时常量池引用,栈帧的内存占用大小仅取决于JVM的实现,其中,局部变量表类似一个数组,JVM线程通过索引的方式读取局部变量表的内容;操作数栈用于保存中间结果和方法保存结果,要求操作数栈中的数据类型和指令必须匹配;运行时常量池引用是JVM实现动态链接的关键,当然也用来支持类字段引用和其它常量的访问;
JVM 规范仅定义栈帧的逻辑结构(如局部变量表、操作数栈),具体实现(如 Slot 是否对齐、指针压缩)由厂商决定。例如,HotSpot 中 Slot 通常为 32 位,long/double占 2 个 Slot,无需 64 位对齐。
参考链接
jvm8s