虚拟机字节码执行引擎--运行时栈帧结构

声明:本文摘抄自《深入理解Java虚拟机》一书,本文完全为自我学习,请感兴趣的同学购买正版,支持原创

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量,操作数栈,动态连接,方法返回地址和其他一些额外的附加信息。每一个方法的调用从开始到执行结束的过程,都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

在编译代码的时候,栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中。因此一个栈帧需要分配多少内存,不会受到程序运行期间变量数据的影响,而仅仅取决于虚拟机的实现。

对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

典型的栈帧结构如下图:


典型的栈帧结构
局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在编译Class文件时,就在方法的Code属性的max_locals数据项中已经确定了该方法需要分配的局部变量表的最大容量。

局部变量的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并没有明确指定一个Slot应占用的内存空间大小。只是很有导向性的说到每个Slot都应该能存放一个boolean, byte, char, short, int, float, reference或returnAddress类型的数据。它允许Slot的长度可以随着处理器,操作系统或虚拟机的不同而发生变化。Reference类型表示对一个对象实例的引用,虚拟机规范并没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少可以通过这个引用做到两点:一是从此引用直接或间接查询到对象在Java堆中的数据存放的起始地址索引,二是从此引用直接或间接的查找到对象所属的数据类型在方法区中的存储的类型信息。returnAddress类型目前已经很少见了,它是为字节码指令jsr,jsr_w和ret服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。

对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java语言中明确的64位的数据类型只有long和double两种。由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果执行的是实例方法(非static方法),那局部变量表中第0为索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

为了尽可能节省栈帧空间,局部变量表的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还伴随一些额外的副作用,例如:在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。

操作数栈

操作数栈(Operand Stack)是一个后入先出(Last In First Out, LIFO)栈。操作数栈的最大深度在编译的时候已经写入到Code属性的max_stacks数据项中。操作数栈中的元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。

当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和读取内容。例如,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令的时候,会将这两个数值出栈并相加,然后将相加的结果入栈。

操作数栈中的元素数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个为long和一个float使用iadd命令相加的情况。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。

方法返回地址

当一个方法开始执行后,只有两种方式可以退出该方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。另一种退出方式是,在方法执行过程中遇到异常,并且这个异常并没有在方法体中得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方使用异常完成出口的方式退出,是不会给他的上层调用者产生任何返回值的。

推荐阅读更多精彩内容