栈帧(frames)
栈帧用于存储数据和部分结果,以及执行动态链接,方法返回值以及异常处理(dispatch exception)。
每次调用方法时都会创建一个新的栈帧。方法调用完成时栈帧会被销毁,无论该完成是正常的还是发生意外(抛出一个没有捕获的异常)。创建栈帧的线程在 Java 虚拟机栈上为栈帧分配空间(§2.5.2)。每个栈帧都有自己的本地变量(§2.6.1),其自己的操作数栈(§2.6.2),以及当前方法所在的类的运行时常数池(§2.5.5)的引用。
栈帧可以有额外的实现特定的信息作为扩展,如调试信息。
本地变量组和操作数栈的大小在编译时确定,并与方法的栈帧的代码(第4.7.3节)一起提供。因此,栈帧数据结构的大小仅取决于 Java 虚拟机的实现,并且这些数据结构所需的内存可以在方法调用的同时进行分配。
任何时候,在指定的线程的控制下,只有一个栈帧,即执行方法的栈帧,是活跃的状态。该栈帧称为当前栈帧(current frame),其方法称为当前方法(current method)。定义当前方法的类是当前类(current class)。对本地变量和操作数栈的操作通常都是指对当前栈帧的操作。
栈帧变成不是当前栈帧如果它对应的方法调用了另一个方法或者它对应方法完成。当调用一个方法时,一个新的栈帧被创建并且当控制转移到新方法时该新建的栈帧变成当前栈帧。在方法返回时,当前栈帧将其方法调用(如果有)的结果传递给上一个栈帧。然后将当前栈帧丢弃,因为上一个栈帧变为当前栈帧。
请注意,由线程创建的栈帧是该线程的本地栈帧,任何其他线程都无法引用。
局部变量
每个栈帧(§2.6)包含称为局部变量(local variables)的变量表(an array of variables)。栈帧的局部变量表(local variable array 译者注:应该就是方法的所有局部变量)的长度在编译时确定,并在类或接口的二进制表示中提供,二进制表示中也有和方法的栈帧相关联的代码(§4.7.3)。
单个局部变量可以容纳boolean
,byte
,char
,short
,int
,float
,reference
或returnAddress
类型的值。一对局部变量可以容纳long
或double
(A pair of local variables can hold a value of type long or double)。
局部变量通过索引进行定位。第一个局部变量的索引为零。当且仅当一个整数介于零和局部变量表的长度减一时,该整数被认为是局部变量数组中的索引。
long
或double
类型的值占据了两个连续的局部变量。这样的值只能使用较小的索引来定位。例如,一个存储在局部变量表中并且索引为 n 的double
类型的局部变量实际上占据了索引为 n 和 n+1 的两个局部变量;但是,索引 n+1 处的局部变量无法加载。但它可以进行存储。但是,这样做会使局部变量 n 的内容无效。
Java 虚拟机不需要 n 是偶数。用直观的术语,long
和double
类型的值在局部变量数组中不需要64位对齐。实现者可以自由地决定使用为该值保留的两个局部变量来表示此类值的适当方法。
Java 虚拟机在方法调用上使用局部变量来传递参数。在类方法(class method)调用上,任何参数均以从局部变量 0 开始的连续局部变量传递。在实例方法(instance method)调用上,局部变量 0始终用于传递对实例方法被调用的对象的引用(在 Java 变成语言中用 this
表示)。之后的任何参数的传递使用的是从局部变量 1 开始的连续的局部变量。
操作数栈
每个栈帧(第2.6节)包含一个名为操作数栈(operand stack)的先进后出(last-in-first-out, LIFO)的栈。栈帧的操作数栈的做大深度在编译时决定,并与方法栈帧的相关代码一起提供(第4.7.3节)。
在上下文清楚的地方,我们有时将当前栈帧的操作数栈称为操作数栈。
当创建包含该操作数栈的栈帧时,该操作数栈为空。 Java 虚拟机提供指令来加载常量、局部变量的值或者是成员变量到操作数栈中。其它 Java 虚拟机指令从操作数栈中取出操作数,对其进行操作,然后将结果压回操作数堆栈(push the result back onto the operand stack)。操作数栈还用于准备要传递到方法的参数和接收方法结果。
例如,iadd 指令(§iadd)将两个int
值进行相加。它要求被进行相加操作的两个int
值是操作数栈顶的两个值,这两个值是被之前的指令压入栈中的。两个int
值都从操作数栈中弹出。它们进行相加操作,并将其总和压回操作数栈。子计算可以嵌套在操作数栈上,从而导致包含计算可以使用的值。(Subcomputations may be nested on the operand stack, resulting in values that can be used by the encompassing computation.)
操作数堆栈上的每个条目(entry)都可以容纳任何 Java 虚拟机类型的值,包括long
和double
类型的值。
操作数堆栈中的值必须以适合其类型的方式进行操作。例如,不可以将两个int
值压入操作数栈然后将它们视为一个long
值,也不可以将两个float
值压入操作数栈然后使用 iadd 指令来将它们进行相加。少数 Java 虚拟机指令(dup指令(§dup)和swap指令(§swap))在运行时数据区域作为原始值操作,而无需考虑其特定类型;这些指令的定义为不能用于以修改或分解单个值的方式来进行操作。这些对操作数栈操作的限制是通过class
文件验证(第4.10节)强制执行的。
在任何时间点,一个操作数栈具有一个相关联的深度,其中long
或double
的值构成了两个单位的深度,并且任何其他类型的值都贡献一个单位的深度。
动态链接
每个栈帧(§2.6)包含对当前方法对应的类型的运行时常量池(§2.5.5)的引用,以支持方法代码的动态链接(dynamic linking)。方法的class
文件代码是指通过符号引用被调用的方法和通过符号引用被访问的变量。动态链接将这些符号方法引用转化为具体方法引用,根据需要加载类,以解决尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关的存储结构中的正确的偏移量。
方法和变量的这种后期绑定使其他类的更改方法在方法中使用的可能性较小。(This late binding of the methods and variables makes changes in other classes that a method uses less likely to break this code.)
方法调用正常完成(Normal Method Invocation Completion)
当此次调用没有抛出异常方法调用正常(completes normally)完成,不管该异常是直接从 Java 虚拟机抛出或者是从显式的 throw
语句抛出。如果当前方法的调用正常完成,则可以将值返回到调用方法。当调用方法执行返回指令之一(§2.11.8)时,就会发生这种情况,其选择必须符合返回值的类型(如果有返回值)。
在这种情况下,使用当前栈帧(§2.6)来恢复调用者的状态,包括其局部变量和操作数栈,调用者的程序计数器适当地递增,以跳过方法调用指令。然后,将返回的值(如果有)压入该栈帧的操作数栈,调用方法正常在其栈帧中执行。
方法调用发生意外(Abrupt Method Invocation Completion)
当此次调用造成 Java 虚拟机抛出异常并且异常没有被处理时方法调用发生意外(§2.10)。执行athrow指令(§athrow)也会导致异常被显式抛出,如果当前方法未捕获异常,会导致方法调用发生意外。意外完成的方法调用永远不会将返回值返回给其调用者。