虚拟机字节码执行引擎(卷五)

虚拟机字节码执行引擎。

0x00 运行时栈帧结构

每个方法从调用开始至结束,都对应一个站帧在虚拟机入栈出栈的过程。

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 返回地址
  • 附加信息

局部变量表

局部变量表是一组变量存储空间,存储方法参数和局部变量。Code属性中的max_locals确定了它的最大容量。

  1. 局部变量表容量以变量槽(Slot)为基本单位。Slot可以复用。
  2. 除了long和double使用两个Slot外,其他都使用1个。
  3. 如果为实例方法,第0位为this。
  4. 局部变量不存在准备阶段,变量必须赋予初始值,不然虚拟机不接受。

操作数栈

操作数栈是一个LIFO栈,max_stacks确定栈的深度。

  • 操作数栈的元素类型必须与字节码指令严格匹配。
  • 在编译阶段由编译器保证。
  • 在类校验阶段再次验证。

动态链接

每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用。

返回地址

只有两种方式退出方法。

  1. 方法执行过程中遇到返回的字节码指令。
  2. 方法执行过程中遇到一次,并且在方法中没有相应的处理。(该方式不产生返回值)

常见的方法退出操作:1恢复上层方法的局部变量表和操作数栈,2将返回值压入上层栈中,3PC计数器执行下一条指令。
一般把动态链接、返回地址、附加信息统称为栈帧信息。

方法调用

方法调用阶段的目的只有一个:确定被调用方法的版本,不涉及方法内部的具体运行过程。

0x00 解析

在类加载的解析阶段,会将一部分符号引用转换为直接引用,前提是方法在运行之前有一个确定的调用版本,且这个方法的调用版本在运行期间不可变。这类方法的调用称为解析(Resolution)。

编译期间可知,运行期间不可变的方法,主要包括静态方法和私有方法。它们适合在类加载阶段进行解析。

  • invokestatic:调用静态方法。
  • invokespecial调用实例构造器init方法、私有方法、父类方法。
  • invokevirtual:调用所有虚方法。
  • invokeinterface:调用接口方法。在运行时在确定实现该接口的对象。
  • invokedynamic:在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。

只要能被invokestatic和invokespecial调用的方法,都可以在解析阶段确定调用版本,符合的有静态方法、私有方法、构造器方法、父类方法4中。它们在类加载时将符号引用解析为直接引用,这些方法称为非虚方法,以及一个final修饰的方法,虽然final修饰的方法编译出来的指令为invokevirtual,但是虚拟机明确规定final方法是一种非虚方法。

分派

分派(Dispatch)调用可能为静态调用,也可能为动态调用,依据宗量数可分为单分派很多分派。

静态分派

Human x = new Man(); Human称静态类型(static type),Man称为实际类型(actual type)。

  • 静态类型的变化只在使用中出现,变量本身的静态类型不会被改变,且最终静态类型编译期间可知。
  • 实际类型变化的结果在运行期间才可确定。
  • 编译器在重载是通过参数的静态类型判定。
  • 所有依赖静态类型定位方法执行版本的分派称为静态分派,典型的有重载
  • 静态分派发生在编译阶段。
  • 字面量没有显式的静态类型。

动态分派

重写是动态分派的一种体现,使用的指令时invokevirtual。
invokevirtual指令的多态查找步骤:

  1. 找到操作数栈顶第一个元素指向的对象的实际类型C。
  2. 如果C于常量中的描述符和简单名称相符,且访问权限通过,查找结束。
  3. 否则,继续下一个变量。
  4. 始终没找到,抛出AbstractMethodError。

单分派、多分派

方法的接收者和参数统称宗量。Java是一门静态多分派、动态单分派的语言。