Java 虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。 如下图(栈帧的概念结构)所示:

image.png

1. 局部变量表 (重要)

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

局部变量表的容量以变量槽(Variable Slot)为最小单位,《Java 虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小。一个变量槽可以存放一个 32 位以内的数据类型,Java 中占用不超过 32 位存储空间的数据类型有 boolean、byte、char、short、int、 float、referencereturnAddress 这 8 种类型。

第 7 种 reference 类型表示对一个对象实例的引用,《Java 虚拟机规范》既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。

第 8 种 returnAddress 类型目前已经很少见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,某些很古老的 Java 虚拟机曾经使用这几条指令来实现异常处理时的跳转,但现在也已经全部改为采用异常表来代替了。

对于 64 位的数据类型,Java 虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java 语言中明确的 64 位的数据类型只有 longdouble 两种。

这里把 long 和 double 数据类型分割存储的做法与 “long 和 double 的非原子性协定” 中允许把一次 long 和 double 数据类型读写分割为两次 32 位读写的做法有些类似,读者阅读到本书关于 Java 内存模型的内容时可以进行对比。不过,由于局部变量表是建立在线程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题。

2. 操作数栈 (重要)

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。操作数栈的每一个元素都可以是包括 long 和 double 在内的任意 Java 数据类型。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种
字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作

这里列举一个局部变量表和操作数栈交互的一个案例:

分析题:a++ + ++a 的执行结果,案例代码如下:

  1. public class Demo3_2 {
  2. public static void main(String[] args) {
  3. int a = 10;
  4. int b = a++ + ++a + a--;
  5. System.out.println(a);
  6. System.out.println(b);
  7. }
  8. }

上面 a、b 的结果是怎样得来的呢?

分析

  • iinc 指令是直接在局部变量桶位(slot)上进行运算。
  • iload指令是用于读取变量
  • a++++a 的区别是先执行 iload还是 先执行iinca++是先 iloadiinc++a相反。

对虚拟机指令不清楚的去看一下这篇文章:JVM_07 类加载与字节码技术 (字节码指令)

bipush 10 操作是把a = 10 放入操作数栈:

6 运行时栈帧结构 - 图2

istore 1 操作,把操作数栈中的 10 弹出,放入到局部变量表的槽位 1 中:

6 运行时栈帧结构 - 图3

③ 接下来执行a++操作,我们上边提前说明了,a++是先执行iload读取,再执行iinc 加 1

  • iload 1 将 变量a=10,读取到操作数栈 stack 中:

6 运行时栈帧结构 - 图4

  • 执行iinc指令,在局部变量表上对 a 进行 +1 操作,这时候 a 为 11:

6 运行时栈帧结构 - 图5

④ 下面执行++a操作,先iinciload

  • 执行iinc指令,在局部变量表上对 a 进行 + 1 操作,这时候 a 为 12:

6 运行时栈帧结构 - 图6

  • iload 1 将局部变量表中a=12,读取到操作数栈 stack 中:

6 运行时栈帧结构 - 图7

⑤ 下面进行 a++ + ++a 操作,在操作数栈中进行相加,得到结果 22,这时候第 1 个加法完成:

6 运行时栈帧结构 - 图8

⑥ 下面执行第二个加法(a++ + ++a)+ a--操作:

  • a-- 先执行 iload 命令,在执行 inc 1,-1命令,如下,先将局部变量表中的 12 读取到操作数栈:

6 运行时栈帧结构 - 图9

  • 接下来执行 inc 1,-1命令,在局部变量表中进行 - 1 操作,此时局部变量表中的值由 12 减为 11:

6 运行时栈帧结构 - 图10

  • 在操作数栈中,执行第二次加法运算,得到结果为 34:

6 运行时栈帧结构 - 图11

⑦ 最后将操作数栈中的数据弹出到局部变量表中,赋值 2 号槽位 b=34:

6 运行时栈帧结构 - 图12

因此程序运行结果得到:a 为 11,b 为 34

3. 动态连接 (了解)

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

我们知道 Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

4. 方法返回地址 (了解)

当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为 “正常调用完成”(Normal Method Invocation Completion)。

另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是 Java 虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为 “异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。

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

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令等。

5. 附加信息 (了解)

《Java 虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在讨论概念时,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
[

](https://blog.csdn.net/weixin_43591980/article/details/119917133?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165055200816780261977680%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=165055200816780261977680&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~times_rank-5-119917133.nonecase&utm_term=%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3&spm=1018.2226.3001.4450)