JVM-字节码执行引擎
标签:JVM

字节码执行引擎

输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

1. 运行时栈帧结构

Stack Frame 是用于支持虚拟机进行方法调用和方法执行的数据结构,它是运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表操作数栈动态连接方法的返回地址等信息。

位于栈顶的是栈帧是有效的,称为当前帧,与这个栈帧管理的方式当前方法。

1.1 局部变量表

它是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量

最大深度写在了Code属性的 max_locals

局部变量表的容量以变量槽(Slot)为最小单位,它允许Slot的长度随处理器,操作系统或虚拟机的不同而变化。

对于long和double使用两个slot以高位对齐的方式,由于局部变量表在栈上,是线程私有的,所以不会引起线程安全问题。

如果访问的是32位数据类型的变量,索引n表示使用第n个Slot,对于64位变量使用n和n+1,不允许使用其中的一个。

方法执行时,虚拟机使用局部变量表完成从参数值参数变量列表的传递过程,如果执行的是非static方法,那么局部变量表第0位索引的slot默认是用于传递方法所属对象实例的引用,在方法中可以通过 this 来访问。

为了节省栈帧空间,局部变量表中的slot是可以重用的

1.2 操作数栈

最大深度写在了Code属性的 max_stacks

操作数栈每一个元素可以存放任意的Java数据类型。但必须与字节码指令的序列严格匹配。

1.3 动态连接

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

字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或在第一次使用时转化为直接引用,这叫静态解析。

另一部分在每一次运行期间转化为直接引用,这部分称为动态连接。

1.4 方法返回地址

方法退出一共有两种情况,一种是方法返回指令,一种是遇到了异常

正常退出时,调用者的PC计数器值可以作为返回地址

遇到异常时,返回地址要根据异常处理器来决定。

退出时可能有的操作:

  1. 恢复上层方法的局部变量表和操作数栈
  2. 把返回值(如果有的话)压入调用者栈帧的操作数栈中
  3. 调用PC计数器的值指向方法调用指令后的一条指令

2. 方法调用

方法调用不等于方法执行,方法调用的唯一任务就是确定被调用方法的版本,即调用哪一个方法,还没有涉及到运行。

由于Class文件中的编译工程部包含传统编译的连接过程,一切方法调用在Class文件中都是符号引用,而不是方法在实际执行时的入口地址(直接引用) ,这个特性让Java有了更多的拓展,可以在类加载或者运行期间确定到底使用哪个版本的方法。

2.1 解析

在类加载阶段转化为直接引用的前提是:方法在程序运行之前就有一个可以确定的调用版本,并且这个版本在运行期间是不会发生改变的。

主要包括静态方法(与类型直接关联)和私有方法(外部不可访问)

与之对应的方法调用指令有

只要能被上面这两条指令调用的方法都可以在类加载阶段将符号引用转化为直接引用,称为非虚方法

由于final方法不能被覆盖,没有其他版本,故final方法也是非虚方法

除了上面这些其他的都是虚方法

2.2 分派

分派(Dispatch)调用则可能是静态的也可能是动态的,根据分配的宗量数可以分为单分派和多分派,两两组合,一共有四种分派方式(静态单分派,静态多分派,动态单分派,动态多分派)

2.2.1 静态分派

虚拟机在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的,静态类型在编译时可知的,因此在编译阶段,Javac编译器会根据参数的静态类型选择决定使用哪个重载版本。

故依据静态类型来定位方法的执行版本的分派动作称为静态分派。典型应用是重载,发生在编译阶段。

解析和分派之间的关系不是二选一的排他关系,它们只是在不同层面上去筛选不同和确定目标方法的过程。前面的说过静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程是通过静态分派来完成的。

2.2.2 动态分派

我们在运行期根据实际类型确定方法执行版本的分派过程称为动态分派,即方法的重写。

2.2.3 单分派与多分派

方法的接收者方法的参数统称为方法的宗量。根据这个关系,可以划分为单分派和多分派两种,单分派指根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

静态分派会根据接受者和参数一起选择故又叫静态多分派。

动态分派是确定了接受者只会根据参数选择故又叫动态单分派。

  • 6 min read

CONTRIBUTORS


  • 6 min read