JVM 的栈帧

栈帧是支持 JVM 进行方法调用和执行的数据结构,它是 JVM 运行时的数据区域的栈元素,其中包含了方法的局部变量表、操作数栈、动态链接方法,和返回地址等信息。

局部变量表和操作数栈的空间,在编译期就已经可以确定下来,并会随着方法表的 code 属性一并提供给 JVM,所以每个栈帧的空间不会受运行时数据的影响,而仅取决于 JVM 的实现。

每当一个方法被调用都会生成一个栈帧,并在方法执行完毕后被销毁,并且对于每个活动的线程,只有栈顶的栈帧是活动的,这个栈帧被称为 “活动栈帧”,与其相关联的方法被称为 “活动方法”,以及与其相关联的类被称为 “活动类”。

局部变量表

每个栈帧中都会有一个被称为 “局部变量表” 的数组,其中保存着方法的局部变量。局部变量表的大小在编译期就已经确定下来,并保存在 class 文件的 code 区。各个变量可通过数组下标的方式被定位到,对于需要占用两个元素的数据类型,比如 longdouble,其对应的下标使用较小的那个值。

在方法执行时,JVM 使用局部变量表完成参数值到参数列表的传递过程的。如果调用的是类方法,那么参数会从局部变量表第 0 位开始向后排列。如果调用的是实例方法 (非 static 方法),则局部变量表第 0 位默认用于传递方法所属对象的实例的引用,在方法中使用 this 关键字可以访问到这个隐含的参数,其余的参数则从第 1 位开始向后排列;在参数表分配完毕后,方法体内部定义的变量会按照其顺序和作用域分配剩余的位置。

操作数栈

每个栈帧中都有一个被称为 “操作数栈” 的栈。操作数栈的最大深度也是在编译期就可以确定下来,并保存在 class 文件的 code 区。

在栈帧创建初期,其中的操作数栈是空的。JVM 提供了一系列的指令,用于将值压入操作数栈,同时也有指令来从操作数栈中取出值并进行计算,并将计算结果压入操作数栈。比如 iadd 指令会从操作数栈中取出最顶部的两个 int 数值,将其相加,然后将结果压入操作数栈。

压入操作数栈的元素的类型必须与指令的要求严格匹配,比如使用 iadd 指令将一个 float 和一个 double 相加是不允许的,这一点不仅在编译期会被严格确定,在类校验阶段也会进行检查。

动态链接

每个栈帧都包含一个指向运行时常量池的引用,用来支持方法调用过程中的动态链接。

字节码中的方法调用指令会以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用时转化成直接引用,这种称为静态解析;另一部分将在每一次运行期间转化为直接引用,这种称为动态链接。

返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法:执行方法返回的指令,和遇到未处理的异常。

执行方法返回的指令称为 “正常方法调用出口 (Normal Method Invocation Completion)”,在这种情况下,如果方法有返回值,那么返回值将会被传递到上方的调用者。此时,当前栈帧将被用来恢复调用者的状态,包括调用者的本地变量表和操作数栈,并会修改 pc 寄存器的值来跳过方法调用指令。

当方法执行期间遇到了异常,并没有找到对应的异常处理器时,导致的方法返回称为 “异常方法调用出口 (Abrupt Method Invocation Completion)”,在这种情况下将不会有值被传回上方调用者。

附加信息

虚拟机规范允许具体的 JVM 实现增加一些规范中没有的信息到栈帧中,比如调试信息等,这些信息的内容将取决于 JVM 的具体实现。