概述(Stack Frame)
- 栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。
- 栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。
每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的,并写入到方法表的code属性中。
栈帧结构图:
在介绍栈帧的各个部分时,我们先来理解一下虚拟机是如何执行一个方法的,这样我们才能理解为什么栈帧需要这些部分,这些部分分别提供了什么功能。首先我们的方法被编译成了字节码,并生成了可执行的命令。通过程序计数器,虚拟机会一行一行的执行命令,直到进入一个新的方法入口,对应虚拟机栈也就是新的栈帧入栈,当前栈帧改变,又或者遇到返回指令或出现异常结束了方法,对应虚拟机也就是出栈。
局部变量表
- 局部标量表 是一组变量值的存储空间,用于存放 方法参数 和 局部变量。在Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量。
- 局部变量表的基本单位为变量槽(slot,下文有单独的介绍);局部变量表存放的是方法参数和局部变量;虚拟机是通过索引定位的方式使用局部变量表。
- 当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用(reference),即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。
- 为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。因为即使是一个方法内,也是存在作用域的,当离开了某些变量的作用域之后,这些变量对应的 Slot 空间就可以交给其他变量使用。但是这种机制有时候会影响垃圾回收行为,原因很简单,当离开某个作用域时,如果没有新的变量值覆盖之前作用域内的变量(指reference)空间,那么当垃圾回收时,则该引用对应的java堆中的内存则不允许被回收,因为局部变量表中还存在该引用。所以问题在于虚拟机并没有主动清理局部变量表中离开作用域的变量值,而是采用新盖旧的方法被动清理。
- 所以很明显,局部变量表的作用就是记录执行该方法时会使用到的变量值,它可以说这个方法的数据池,是我们方法中变量的化身,相当于把我们方法中所需要的变量整合成一个数组对象或集合对象,这个对象的名称就叫做局部变量表。
变量槽(Variable Slot)
- 虚拟机规范中并没有明确说明一个Slot应占用的内存空间大小,只是很有“导向性”的说一个 Slot 可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型。正常来讲以上的数据只需要用32位长度的内存空间,但是虚拟机规范并没有定死,它允许slot的长度随着处理器、操作系统或虚拟机的不同而发生变化。不过即使在64位虚拟机中使用64位长度的内存空间来实现slot,虚拟机仍要使用对齐和补白的手段让slot外观上看起来有32位虚拟机的一致(so why???)。
- 所以正常来说一个slot的占用32位的长度内存,可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型,而 对于64位的 long 和 double 变量而言,虚拟机会为其分配两个连续的 Slot 空间。
操作数栈
- 操作数栈也常被称为操作栈,。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,这里的栈就是指操作数栈。
- 操作数栈的每个位置上可以保存一个java虚拟机中定义的任意数据类型的值,包括long和double。
- 那操作数栈的作用是什么?我们上一段代码就明白了:
iload_0 // 将局部变量表0号索引的值入操作数栈
iload_1 // 将局部变量表1号索引的值入操作数栈
iadd // 操作数栈去除前两位相加,放入栈顶
istore_2 // 操作数栈顶元素出栈,放入局部变量表2号索引
对应Java代码
int a=1;
int b=2;
int c=a+b;
- 很明显,操作数栈是方法执行算术运算或者是调用其他的方法进行参数传递的时候时的媒介,这就是“基于栈的执行引擎”。
- 操作数栈中的元素类型必须与字节码指令序列严格匹配,比如不能用iadd 指令去加两个long类型的数据。当然这些基本数据类型的校验在编译期中会校验,编译是无法通过两个long类型加为int类型的代码。当然指令还有很多种,不要像我一开始就产生困惑iadd这么简单的指令如何解释那么复杂的代码的。
- 当一个方法刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈的操作。
- 在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
动态连接
- 每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
- Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- 我也没太看懂,我大概的理解是这样的,一个方法中,不止传进来的参数和内置变量,还会有要调用的方法,成员变量和成员变量,在栈帧中不是直接就保存这些信息的直接引用,而是这些“符号”(理解为存储值的地址,也就是引用)的引用(动态链接)。(第一次)要使用到的时候,才会根据动态链接转换为直接的地址(然后放到局部变量表里面么?)。如果理解有误,欢迎纠正。
方法返回地址
当一个方法开始执行以后,只有两种方法可以退出当前方法:
- 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
- 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
当方法返回时,可能进行3个操作:
- 恢复上层方法的局部变量表和操作数栈
- 把返回值压入调用者调用者栈帧的操作数栈
- 调整 PC 计数器的值以指向方法调用指令后面的一条指令
后记
- 现在我们来想一想,一个方法解释执行的过程中所必须条件的是不是以上的部分都已经提供了。存储数据的区域,执行数据的区域,关联外部数据的区域,方法返回地址。整理一下想想原理也挺简单的~
- 再抄录一段jvm虚拟机的内容:refrence类型如何定位到对象实例及类型
- 句柄式
- 句柄式
- 直接引用