第八章 虚拟机字节码执行引擎

8.2 运行时栈帧结构

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

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法返回地址
  • 一些额外信息

​ 只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。

8.3.1 局部变量表

局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数方法内部定义的局部变量

​ 局部变量表的容量以**变量槽( Variable Slot)**为最小单位。

1.变量槽:

​ Java 中占用不超过 32 位存储空间的数据类型有boolean、 byte、 char、 short、 int、 float、 reference和 returnAddress 这 8 种类型。

前面6中不需要解释。

第7种:reference表示对一个对象实例的引用,《Java 虚拟机规范》既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。reference需要做到以下两件事:

  • 从根据引用直接或间接地查找到对象在 Java 堆中的数据存放的起始地址或索引
  • 根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
    第8种已经很少见了,就不谈了。

​ 对于 64 位的数据类型, Java 虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java 语言中明确的 64 位的数据类型只有 long 和 double 两种。这里把 long 和double 数据类型分割存储的做法与“long 和 double 的非原子性协定”中允许把一次 long 和double 数据类型读写分割为两次 32 位读写的做法有些类似。

​ 不过,由于局部变量表是建立在线程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题

当一个方法被调用时, Java 虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。

为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的。

8.2.2 操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出( Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code 属性的 max_stacks 数据项之中。

Java 虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。

8.2.3 动态连接

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

8.2.4 方法返回地址

两种返回:

  1. 正常调用完成
  2. 遇到了异常

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。

8.3 方法调用

Class 文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。

8.3.1 解析

所有方法调用的目标方法在 Class 文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution)

编译期可知,运行期不可变的方法:静态方法和私有方法

Java虚拟机支持的5条方法调用字节码指令:

  • invokestatic。用于调用静态方法。

  • invokespecial。用于调用实例构造器<init>()方法、私有方法和父类中的方法

  • invokevirtual。用于调用所有的虚方法。

  • invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象

  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行
    该方法。前面 4 条调用指令,分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指
    令的分派逻辑是由用户设定的引导方法来决定的,也就是并不是由虚拟机决定的。

​ 解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。

8.3.2 分派

1.静态分派------重载
发生在 编译阶段

Human man = new Man();

​ 我们把上面代码中的“Human”称为变量的“静态类型”(Static Type),或者叫“外观类型”(Apparent Type),后面的**“Man”则被称为变量的“实际类型”(Actual Type)或者叫“运行时类型”(Runtime Type)。静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的**;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

​ 两个静态类型相同,而实际类型不同的变量,虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。
2.动态分派------重写

根据实际类型来进行动态分派。

如何进行的呢?invokevirtual运行指令大致分以下几步:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C。

  2. 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如

果通过则返回这个方法的直接引用,查找过程结束;不通过则返回 java.lang.IllegalAccessError

异常。

  1. 否则, 按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程。

  2. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常

​ 正是因为 invokevirtual 指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的 invokevirtual 指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是 Java 语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

3.单分派和多分派

方法的接收者方法的参数统称为方法的宗量

Java 语言是一门静态多分派、动态单分派的语言

4.虚拟机动态分派的实现

​ 动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法,Java虚拟机用虚方法表来解决。
方法表
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。

​ 虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕。

8.5 基于栈的字节码解释执行引擎

可以看看书中字节码解释的示例

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倜傥村的少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值