我们的Java运行时数据区包括线程独占区中的Java虚拟机栈,Java虚拟机栈中的“元素”就是栈帧。
什么是栈帧?
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
栈帧中都包含什么?
每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全的确定了,并且写入到方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
接下来详细讲解一下栈帧中的局部变量表,操作数栈,动态连接,方法返回值等各部分的作用和数据结构。
1.局部变量表
什么是局部变量表?
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_local数据项中确定下来了。
单位槽是什么?
局部变量表的容量以单位槽为最小单位,虚拟机规范中并没有明确指定一个Solt应占的内存空间大小,只是说Solt应该可以存储下小于或者等于一个字节类型的数据。
所有类型的数据都占一个单位槽吗?
对于64位的数据类型,虚拟机会以高为对其的方式为其分配两个连续的Solt空间。这里把long和double类型分割存储的做法是不符合数据的原子性操作的,那么会引发并发问题吗?答案是不会,因为我们的Java虚拟机栈处于线程独占区,无论连个连续的Solt是否为原子操作,都不会引起数据安全问题。
局部变量表的填充顺序
局部变量表中变量的分配顺序:先分配方法所在类的实例的引用“this”关键字(作为参数传入)接下来是其余的参数,参数表分配完毕后再根据方法体内部定义的变量顺序和作用域分配其余Solt。
Solt空间重用
为了节省栈帧的空间,局部变量表中的Slot是可以重用的,方法体重定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Solt就可以交给其他变量使用。不过这样的设计除了节省栈空间以外还会伴随一些额外的副作用,例如在某些情况下,Solt复用会直接影响到垃圾收集器的行为。
2.操作数栈
操作数栈(Operand Stack)也称为操作站,它是一个后进先出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。
操作数栈中元素的数据类型必须与字节指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段和数据分析中还要再次校验这一点。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全独立的。但是在大多数虚拟机实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,提高效率。
3.动态连接
每个栈帧都包含一个指向运行是常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。我们直到Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法符号引用作为参数。这些符号引用一部分在类加载阶段或者第一次使用的时候就转化为直接引用,这部分成为动态连接。
4.方法返回值
正常完成出口
当一个方法开始执行后,只会有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回值的字节指令。这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型间根据遇到何种方法返回值指令来决定,这种退出方法的方式称为正常完成出口。
异常完成出口
另一种退出方式是,载方法执行过程中遇到了异常,并且这个异常没有在方法体内的得到处理,无论是虚拟机内部产生异常还是athrow字节码指令产生的异常,只要在本方法异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口。
方法退出之后会如何?
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC 计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器的值。而方法异常退出时,返回地址时通过异常处理器来确定的栈帧中一般不会存储这部分信息。
方法退出之后执行那些操作?
方法退出的过程实际上就等同于把当前栈帧出栈,因为退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。