2.6. 栈帧
栈帧用于存储数据和部分结果,同样也用于执行动态链接,返回方法的值和分派异常。
当方法被调用的时候会创建一个新的栈帧。当一个方法调用结束时,它对应的栈帧就被销毁了,不管是正常调用结束还是意外结束(抛出了未被捕获的异常)。栈帧分配在线程创建的虚拟机栈中。每个栈帧都有自己的局部变量表,操作数栈,以及当前方法的类的运行时常量池的引用。
可以使用附加的特定于实现的信息来扩展帧,例如调试信息
局部变量表和操作数栈在编译时期就确定了,并且通过栈帧关联的方法的code提供。因此栈帧的大小仅仅取决于虚拟机的实现,和方法调用时这些结构所分配的内存。
在一个给定的线程上,任何时刻只有正在执行的方法的栈帧时活动的。这个帧被称之为当前帧,它的方法就是当前方法,对应的类就是当前类。局部变量和操作数堆栈的操作通常是指当前帧上的操作。
一个帧的方法调用另外一个方法,或者这个帧的方法结束了,那么这个帧不再是当前帧了。当一个方法被调用,一个新的栈帧就会被创建,当控制权转移到这个新方法时,这个帧就变成当前帧。在方法返回时,当前帧将其方法调用的结果(如果有)传递回前一帧,然后当前一帧成为当前帧时丢弃当前帧。
注意线程创建的栈帧是属于这个线程的,不可能被别的线程引用。
2.6.1 局部变量表
每个栈帧都包含一组变量,称之为它的局部变量表。帧的局部变量数组的长度在编译时确定,并以类或接口的二进制表示形式提供,存储在帧相关的方法的code属性中。
一个局部变量可以保存boolean,byte,char,short,int,float,reference或者returnAddress的值。一对局部变量可以保存long或者double的值。
局部变量表根据索引定位。第一个局部变量的索引是0。如果一个整数大于等于0并且小于局部变量表的长度,就是这个局部变量表的索引值。
long或者double的值占据两个连续的局部变量,这样的值只用两个变量中最小的索引值来定位。例如,double的值存在局部变量表的索引n处,那么实际是它也占据了n+1的局部变量位置,但是无法根据n+1的索引来加载这个变量。索引值为n+1的变量可以被写入,但是,这样做会使局部变量n的内容无效。
上文中提及的局部变量n的n值并不要求一定是偶数,Java虚拟机也不要求double和long类型数据采用 64 位对齐的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个 double 或 long 类型的值。
java虚拟机使用局部变量表在方法调用时传递参数。当类的方法被调用时,所有参数都被传递到索引从0开始的连续的局部变量表中。当一个实例的方法调用时,索引为0的局部变量总是被用来传递这个实例对象的引用(java编程语言中this)。后续其他参数将会传递到从1开始的连续的局部变量表中。
2.6.2 操作数栈
每个栈帧都有一个后进先出(LIFO)的栈,即操作数栈。一个栈帧的操作数栈的最大深度在编译器确定,由这个帧所在的方法的code属性提供。
当上下文清晰的适合,我们有时会把当前帧的操作数栈简称操作数栈。
栈帧刚创建时,他的操作数栈时空的。java虚拟机提供一系列指令从局部变量表或者字段中去加载常量或者变量到操作数栈中。其他的一些虚拟机指令从操作数栈中取出操作数,对它们进行操作,然后将结果推入到操作数栈中。操作数栈也被用户准备参数参数传递给方法和接受方法的结果。
例如,iadd指令用户将两个int数相加。它需要操作数栈顶部的两个int数字进行相加,这两个数字需要被之前的指令提前入栈。两个int值从操作数栈中出栈,然后它们相加,将他们之和入栈到操作数栈。子计算可以操作数栈中嵌套进行,它们的结果能够被外围的计算使用。
每个入栈的成员都能保存任何一个虚拟机类型,包括long类型和double类型。
必须使用符合它们类型的操作去操作操作数栈中的值。例如,不可能入栈两个int值,然后将它们当成long类型来操作,或者入栈两个两个float值,接着使用iadd命令来操作。只有一小部分虚拟机指令(如dub和swap指令)操作运行时数据区域时,将其中的值当作原始值,而不用考虑它们的特殊类型;这些指令不能用于修改或者分解单个值。这些操作数栈上面的操作限制时用class文件验证来强制保证的。
任何时刻,一个操作数栈都有确定的深度,long或者double占用两个单元的深度,其他类型只会占用一个类型的深度。
2.6.3 动态链接
每一个帧都包含一个运行时常量池的引用,用于当前方法支持方法代码的动态链接(dynamic linking)。一个方法在class文件中的code,描述一个方法调用和变量访问时通过符号引用(symbolic references)。动态链接将这些方法的符号引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关的存储结构中的适当偏移。
方法和变量的这种后期绑定使得,当前方法使用了其他类中的方法或者变量发生变化时,不太可能破坏当前方法的代码。
2.6.4 方法的正常调用完成
一个方法调用正常完成是指,这个调用没有导致异常出现,既不是java虚拟机直接抛出也不是执行了一个具体的throw语句。如果当前方法的调用正常完成,然后可能返回一个值给调用者。当调用的方法执行了某一种return指令,才会出现返回值,return指令必须选择符合返回值类型的指令。
在这种情况下,当前帧的目的是用于恢复调用者的状态,包括其局部变量和操作数堆栈;然后调用者的程序计数器适当地递增以跳过方法调用指令。然后在调用者的方法的帧中继续正常执行,返回值(如果有)被推送到该帧的操作数堆栈。
2.6.5 方法的异常调用完成
一个方法异常调用结束是指,jiava虚拟机指令执行该方法时导致了虚拟机抛出一个异常,并且这个异常没有在该方法中处理。执行athrow指令同样会导致一个异常被显示的抛出,如果当前方法没有捕获这个异常,该方法调用会异常完成。方法调用异常结束不会返回值给它的调用者。