高亮的杂货铺

JVM从0到1-运行时栈帧结构

2020-03-14 · 5 min read
JVM Java

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中。

一个线程中的方法调用链可能会很长,从Java程序的角度看,同一时刻,同一个线程里,在调用堆栈的所有方法都同时处于执行状态。但是对于JVM的执行引擎来说,在活动线程中,只有位于栈顶的方法才是生效的,被称作当前栈帧(Current Stack Frame),与这个栈帧所关联的方法被称作当前方法,执行引擎所运行的所有字节码指令都是针对当前栈帧。

局部变量表(Local Variables Table)

局部变量表是以组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,在Java程序被编译为class文件时,就在方法的Code属性的max_locals数据项中明确了该方法所需要的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot)为最小单位,每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。所以大多数虚拟机使用32位长度来作为一个槽的大小,但是虚拟机规范并没有明确说明槽的大小。

reference表示对一个对象实例的引用,虚拟机规范并没有说明他的长度,也没有明确指出这种引用应有什么样的结构。 但是一般要求至少通过这个引用可以获取数据存放的起始地址以及这个对象所属的数据类型在方法区中存储的类型信息。 returnAddress类型已经很少见了。

Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表的最大的变量槽地址,如果是32位数据,则索引N代表使用第N个变量槽,如果是64位的数据,则说明会同时使用N和N+1两个变量槽。

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量表列表的传递过程,即实参到形参的传递。如果是实例方法,那么局部变量表的第0位索引是this对象本身的引用,其余参数按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完成后,再根据方法体内定义的变量顺序和作用域分配其他的变量槽。

为了尽可能的节省栈帧的内存空间,局部变量表中的变量槽是可以重用的,当当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量对应的槽就可以交给其他变量来重用。

还有一点需要注意的是,局部变量不像类变量一样存在准备阶段。

操作数栈(Operand Stack)

操作数栈是一个后入先出的栈,和局部变量表一样,他的最大深度也是在编译时就被写道Code属性的max_stacks数据项中的。 操作数栈的每一个元素都可以是包括long和double在内的任意java数据类型,32位数据类型占据1个栈容量,64位占据2个栈容量。

动态连接(Dynamic Linking)

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

方法返回地址

当一个方法开始执行之后,只有两种方式退出这个方法,一个是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,这种退出方式称为正常调用完成。

另外一种退出方法是在内部执行的过程中遇到了异常,而且这个异常没有在方法体内得到妥善的处理。 一个方法以异常完成出口的方式退出,是不会给他的上层调用者任何返回值的。

无论任何方式退出,在退出之后,都必须要返回到最初方法被调用的地方,程序才能继续执行。方法返回时可能需要再栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态,一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是由异常处理器表来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程等同于把当前栈帧出栈,因此退出时可能的操作由:恢复上层方法的局部变量表和操作数量栈,将返回值压入调用栈帧的操作数栈中,调整pc计数器的值以指向调用指令后面的一条指令等。