基于栈的虚拟机执行引擎

栈帧

栈帧(Stack Frame)是虚拟机中用于实现方法调用和方法执行的数据结构,是虚拟机栈中的基本数据元素。

一个栈帧中主要包含以下内容:

  • 局部变量表

局部变量表用于存储方法的形参以及方法内部定义的局部变量,在Class文件中每个方法的Code属性中已经定义了局部变量表的最大容量。
局部变量表的存储单位是一个Slot,一个Slot可以存放一个32位以内的数据类型,如果是64位数据则占用两个Slot。虚拟机通过索引访问局部变量表中的元素,对于32位数据,索引n即表示访问第n个Slot,对于64位数据,索引n表示访问第n和第n+1个Slot。
调用方法时,局部变量表首先会按照参数列表的顺序存储各个形参,对于非static方法,第一个形参是“this”,即该方法所属对象的实例。然后再按照局部变量定义的顺序分配剩余的Slot。

  • 操作数栈

操作数栈用于暂存方法指令执行过程中的参数以及中间结果,在Class文件中每个方法的Code属性中也定义了操作数栈的最大深度。

  • 动态链接

每个栈帧都会包含一个指向虚拟机方法区中该栈帧所属方法的引用。调用方法的字节码指令是以常量池中指向该方法的符号引用作为参数,这些符号引用有些在类加载过程的解析阶段就转换成直接引用,有些则是在运行时动态转化为直接引用。

  • 方法返回地址

方法返回主要有两种方式:正常的执行结束返回和遇到没有处理的异常返回。正常执行结束返回时,方法的返回地址是调用者的PC计数器的值,这个值则会保存在被调用方法的栈帧中。遇到未处理的异常返回时,返回地址是通过方法的异常表来确定的,返回地址不会存储在栈帧中

方法调用

Class文件中表示被调用方法的都是符号引用,这些符号引用有的可以在类加载的解析阶段确定出被调用方法的直接引用,但有些则要等到运行时才能确定。

Class文件调用方法的字节码指令主要包括:

  • invokestatic
    调用静态方法
  • invokespecial
    调用实例构造方法<init>、私有方法和父类方法
  • invokevirtual
    调用虚方法
  • invokeinterface
    调用接口方法

通过invokestatic指令和invokespecial指令调用的方法在类加载的解析阶段就可以确定其直接引用。这些方法包括:静态方法、实例构造方法、私有方法以及父类方法。通过其他指令调用的方法只能在运行时动态确定其直接引用。

方法调用分派

方法调用分派即定位方法的实际执行版本,包括静态分派和动态分派。

  • 静态分派

静态分派依据静态类型来定位方法的执行版本,静态分派的典型应用是方法的重载(overload

 public class Fruit {
 }
 
 public class Apple extends Fruit {
 }
 
 public class Main {
	 public static void eat(Fruit fruit) {
		System.out.println("eat fruit");
	 }
	 
	 public static void eat(Apple apple) {
		System.out.println("eat apple");
	 }
	
	 public static void main(String[] args) {
		 Fruit apple = new Apple();
		 Main.eat(apple);
	 }
 }

上述例子的输出结果是:

   eat fruit 

虽然apple的实际类型是Apple,但其声明的静态类型是Fruit,因此在分派调用重载的eat方法时,会根据参数的静态类型来选择调用方法的版本。
静态分派的结果实际上在编译阶段就已经确定了,虚拟机只需要在类加载阶段将符号引用替换成相应的直接引用。

  • 动态分派

动态分派即根据动态类型来确定方法的执行版本,动态分派的典型应用是覆盖(override)。

 public class Fruit {
	 public void eat() {
		 System.out.println("eat fruit");
	 }
 }
 
 public class Apple extends Fruit {
	 public void eat() {
		 System.out.println("eat apple");
	 }
 }
 
 public class Main {
	 
	 public static void main(String[] args) {
		 Fruit apple = new Apple();
		 apple.eat();
	 }
 }

上述例子的输出结果是:

eat apple

虚拟机是如何确定实际需要执行的方法版本,以上述例子来说:
(1)虚拟机首先找到局部变量表的第一个元素,即“this”引用,通过“this”引用可以确定该方法所属对象的实际类型是Apple。
(2)在方法区中Class Apple包含的所有方法中查找方法签名与eat() 完全相符的方法,找到后返回该方法的直接引用。
(3)第(2)步中,如果在Class Apple中找不到符合的方法,则按照继承关系从下往上依次查找各个父类。

虚表

invokevritual和invokeinterface指令对应的方法调用采用的都是动态分派,因此动态分派在虚拟机中频繁发生,实际的查找过程并不能像上面所述的那样繁琐低效,虚拟机会在方法区中为每个Class建立一个虚表来保存该Class所有的虚方法。

虚表中保存各个方法的实际入口地址,动态分派过程中实际是使用虚方法(上述例子中的eat方法)在虚表中的索引直接找到对应的方法地址。
如果子类没有覆盖父类的虚方法,则子类中的该方法的入口地址直接指向父类中的对应的虚方法,避免递归查找。


参考资料:《深入理解Java虚拟机》
posted @ 2017-11-27 00:48  jqc  阅读(449)  评论(0编辑  收藏  举报