JVM–详解虚拟机字节码执行引擎之栈帧结构

0 引言

什么是虚拟机字节码执行引擎?它有什么作用?

在说明这个问题之前,先来想想 .class 文件结构与类加载机制。

使用 javac 命令对 Java 程序进行编译,产生的虚拟机字节码存储在 .class 文件中。类加载机制就是将 .class 文件中的字节码加载进 JVM 的方法区并生成这个类的 Class 对象的过程(再次强调不是生成这个类的实例化对象的过程)。

1 虚拟机字节码执行引擎

假设现在有这样一个类:

public class Demo {

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
复制代码

JVM 以 main 方法作为入口,而这些方法在运行之前首先要进行调用,虚拟机字节码执行引擎就是负责对方法进行调用并运行方法的 JVM 组成部分之一

2 运行时栈帧结构

要了解 JVM 对方法进行调用的过程,首先要掌握一个概念:栈帧。栈帧是一种数据结构,栈帧中包括了局部变量表、操作数栈、动态连接、返回地址等信息。

这里写图片描述

  • 栈帧存在于虚拟机栈中,并且是虚拟机栈中的单位元素
  • 每个线程中的不同栈帧对应这个线程调用的不同方法,可以看到栈帧很多,调用的方法链也会很多
  • 在活动线程中,只有当前栈帧有效,与之对应的也就是当前正在执行的方法,此方法被成为当前方法
  • 每调用一个新的方法,此方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧;当一个方法退出的时候,此方法对应的栈帧也相应销毁(出栈)【递归的原理】

栈帧中需要多大的局部变量表,多深的操作数栈在编译成 .class 文件的时候都是已经确定好的,这些信息都存储在方法表中的 code 属性中,因此每个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。

回顾一下方法表及 code 属性的定义。

  • 方法表:.class 文件中的方法表包含了此方法的一些信息:访问标志(public、private 等)、名称索引(指向常量池)、描述符索引(指向常量池,描述符用来描述方法的参数列表以及返回值)、属性表集合。
  • code 属性的定义:code 属性存储在属性表中,而属性表是多种属性的集合。code 属性存放的就是经过编译器编译成字节码指令的 Java 方法里面的代码(里面记录了局部变量表的大小与操作数栈的深度)。

方法表中不一定需要属性表,因为如果这是一个抽象方法,那么这个方法生成的方法表中就不需要存在属性表(这个 Java 方法没有被定义,属性表中的其他属性也无法被生成)。

3 局部变量表

既然每个栈帧对应了每个调用过的方法,那么栈帧中存储的理应是我们平常方法体中所写的 Java 代码。

局部变量表作为组成栈帧的一部分,用于存储方法参数和方法内部定义的局部变量,在 Java 程序编译成 .class 文件的时候,就在 code 属性中的 max_locals 数据项中确定了该方法所要分配的局部变量表的最大容量。

局部变量表被组织为以一个 slot(变量槽)为单位、从 0 开始计数的数组。虚拟机规范中并没有明确规定 slot 的大小,只是说明每个 slot 都应该能存放一个 boolean、byte、char、short、int、float、reference、或 returnAddress 类型的数据。

类型为 short、byte 和 char 的值在存入数组前要被转换成 int 值,而 long 和 double 在数组中占据连续的两项,在访问局部变量中的 long 或 double 时,只需取出连续两项的第一项的索引值即可,如某个 long 值在局部变量区中占据的索引是 3、4 项,取值时,指令只需取索引为 3 的 long 值即可。

举个栗子:

public static int runClassMethod(int i,long l,float f,double d,Object o,byte b {
    return 0;
}
复制代码
public int runInstanceMethod(char c,double d,short s,boolean b) {
    return 0;
}
复制代码

这里写图片描述

如上图,可以看到虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的。并且 runInstanceMethod(实例方法)的局部变量表第一项是个 reference(引用),它指定的就是对象本身的引用,也就是我们常用的 this,但是在 RunClassMethod 方法中,没这个引用,因为 runClassMethod 是个静态方法。

关于 reference 类型,代表的是一个对象实例的引用,这个引用应当可以做到两点:

  1. 从此引用中直接(直接引用)或间接(句柄池)地查找到对象在 Java 堆中存放的起始地址索引
  2. 从此引用中直接(对象头)或间接(句柄池)的查找到对象所属类型在方法区中存储的类型信息

4 操作数栈

同样,操作数栈也是一个先入后出的栈结构。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到了 code 属性的 max_stacks 数据项中,运行期并不会改变。

操作数栈和局部变量区一样,也被组织成一个数组,操作数栈中的每个元素可以是任意的 Java 数据类型,32 位数据类型所占栈容量为 1,64 位数据类型所占栈容量为 2。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。

下面我们通过一幅图片来了解下操作数栈的作用:

这里写图片描述

如果上图中的指令你们无法理解的话,也不要着急,更详细的内容会在之后的第三部分讲解~~

5 动态连接

这部分的内容会在之后的第二部分专门介绍,现在先不讲。

6 方法返回地址

在一个方法开始执行之后,将来这个方法肯定是会退出的。方法的退出分为正常结束和异常终止。如果是通过 return 正常结束,则当前栈帧从线程所拥有的栈中弹出,恢复发起调用的方法的栈。如果方法有返回值,JVM 会把返回值压入到发起调用方法的操作数栈中。

为了处理 Java 方法中的异常情况,栈帧中还必须保存一个对此方法异常引用表的引用。当异常抛出时, JVM 先调用 catch 块中的代码,如果没发现 catch 块,方法立即终止,然后 JVM 用栈帧中的信息恢复发起调用的方法的帧,然后再发起调用方法的上下文重新抛出同样的异常。

7 总结

  1. 每个线程都有一个自己的栈
  2. 栈由栈帧所组成
  3. 每个方法调用对应一个栈帧,当前正在执行的方法对应当前栈帧
  4. 栈帧是一种数据结构,由局部变量表、操作数栈、方法返回地址及其他栈帧信息所组成
  5. 局部变量表中并不存储变量名,而是使用索引来访问存储在局部变量表中的值
  6. 操作数栈只是一个临时存储数据的区域,与栈帧一样,也是使用类似栈的数据结构

8 参考阅读

  1. 深入理解 Java 虚拟机
  2. 深入JVM——栈和局部变量
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享