总体认知
Java虚拟机以方法作为最基本的执行单位,“栈帧”则是用于支持虚拟机进行方法调用和执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。一个Java方法对应虚拟机中的一个栈帧,栈帧里面主要用于存储这个方法的:局部变量表、操作数栈、动态链接、方法的返回地址、附加信息
在编译Java源程序的时候,一个栈帧需要多大的局部变量表、多大的操作数栈,就已经被分析计算出来了,并且写入到了方法表的Code属性中。也就是说一个栈帧需要多大的内存,并不会受到程序运行时期的影响,仅仅取决于程序源码和具体虚拟机实现的栈内存布局形式。
一个线程中,方法的调用链可能会很长,从Java程序的角度看,在调用堆栈的所有方法都同时处于运行状态。但是,对于虚拟机执行引擎而言,只有位于虚拟机栈的栈顶的栈帧对应的方法才是处于运行状态的。这个栈帧也被叫做“当前栈帧”,其关联的方法也被叫做“当前方法”,一个方法从开始被调用到执行结束就是对应的栈帧在虚拟机栈的栈顶栈帧从入栈到出栈的过程。
两个不同的栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多数虚拟机的实现里都会进行一些优化处理:令两个栈帧出现一部分重叠。这样做的原因就是不仅节约了一些空间,还能在进行方法调用时就可以直接共用一部分数据,无需额外的参数复制传递。如下图所示:
局部变量表
局部变量表是一组变量的存储空间,用于存储方法参数和方法内部定义的局部变量。在Java源程序编译成Class文件时,就会在方法的Code属性的max_locals数据项中确定该方法所需要分配的局部变量表的最大容量。
局部变量表的最小存储单位是变量槽。《Java虚拟机规范》中没有明确指明一个变量槽占用的内存空间大小,允许变量槽长度随着处理器、操作系统和虚拟机的不同而发生变化。但是指出了每个变量槽都应该能够存放一个boolean、byte、char、shorint、float、reference、returnAddress类型的数据。对于这8种数据类型,都可以使用32位或更小的物理内存来存储。在32位虚拟机中,虚拟机一般会为每个数据类型分配一个变量槽的空间,而在64位虚拟机中一般会以高位对齐的方式为其分配两个变量槽的空间。
注意: 局部变量表的变量槽是可重用的,为了尽可能节省栈帧空间,若当前字节码PC计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
局部变量表的访问方式是通过索引定位,索引值的范围是从 0 开始至局部变量表最大的变量槽数量。
注意: 局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例的引用,即上图的“this”,它指向堆中当前对象的引用。
操作数栈
当一个方法刚开始执行的时候,这个方法的操作数栈是空的,然后随着方法的执行,会有各种字节码指令往操作数栈中写入(入栈)和提取(出栈)内容。在编译时会在方法的Code属性max_stacks数据项中确定操作数栈的最大深度。操作数栈的每一个元素可以是任意的Java数据类型 ——32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
例子: 整数相加的字节码指令iadd,这条指令在运行的时候要求要求操作数栈中最接近栈顶的两个元素已经存入两个int类型的数值,当执行这条指令时,就会把这两个int值出栈并相加,然后将相加的结果重新入栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池里面指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时就被转化为直接引用,这种转化称为静态解析;而另一部分将在每一次运行期间都转化为直接引用,这部分就称为动态链接。
方法返回地址
方法退出的两种方式:
- 正常退出:执行中遇到任意一个方法返回的字节码指令
- 异常退出:执行中遇到异常且在本方法的异常表中没有搜索到匹配的异常处理器去处理异常
无论采用何种方式退出方法,在方法退出后,都必须返回到最初方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层调用者的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址;方法异常退出时,栈帧中不会保存这部分的信息,需要通过异常处理器表来确定返回地址。
附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如:与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不进行详述。