JVM字节码执行引擎
JVM执行引擎
输入:字节码文件
处理:字节码解析
输出:执行结构
运行时帧栈结构
帧栈(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,是JVM运行时数据区中的虚拟机栈的栈元素。
帧栈储存了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。
每一个方法调用的开始和结束都对应着一个帧栈在虚拟机栈里面入栈和出栈。
局部变量表
局部变量是一组变量值储存控件,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位。
在方法执行时,虚拟机是使用局部变量完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排序,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内的定义的变量顺序和作用域分配其余Slot。
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,是一个后入先出的栈。
操作数栈的每一个元素可以是任意的一个JAVA数据类型,包括long和double。32位数据类型占栈容量为1,64位为2。
当一个方法开始执行时,这个方法的操作数栈是空的,在方法的执行过程中会有各种字节码指令在操作数栈中写入和提取,也就是入栈和出栈操作。
动态连接
每个栈帧都包含一个指向运行时常量池中该帧栈所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。
1.执行引擎遇到任意一个方法返回的字节码指令,称为正常完成出口。
2.在方法执行过程中遇到了异常,并且这个一场没有在方法体内得到处理,称为异常完成出口。
方法退出的本质就是把当前帧栈出栈。
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到帧栈中,一般包括动态连接,方法返回地址和其他附加信息。
方法调用
方法调用唯一的确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
方法调用分为两类:
- 解析调用是静态的过程,在编译期间就完全确定目标方法。
- 分派调用即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派。
解析
在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析。
静态方法和私有方法符合“编译器可知,运行期不变”的原则,不可能通过继承或别的方式重写版本,因此它们适合在类加载的阶段进行解析。虚拟机中提供了以下几条方法调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用
<init>
方法、私有及父类方法,解析阶段确定唯一方法版本 - invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- invokedynamic:动态解析出需要调用的方法,然后执行
分派
- 静态分派:所有依赖静态类型来定位方法执行版本的分派成为静态分派,发生在编译阶段,典型应用是方法重载。
- 动态分派:在运行期间根据实际类型来确定方法执行版本的分派成为动态分派,发生在程序运行期间,典型的应用是方法的重写。
- 单分派:根据一个宗量对目标方法进行选择,Java语言的静态分派属于多分派类型。
- 多分派:根据多于一个宗量对目标方法进行选择,Java语言的动态分派属于单分派类型。
注:方法的接收者与方法的参数统称为方法的宗量。
JVM动态分派的实现
JVM通过为类在方法区建立一个虚方法表来提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口父类的一致,如果子类重写了这个方法,子类的虚方法表中该方法的实际入口将会被替换为指向子类实现版本的入口地址。
基于栈的字节码解释执行引擎
解释执行
Javac编译器完成了程序代码经过词法分析,语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在JVM之外进行的,而解释器在虚拟机内部,所以Java程序编译就是半独立的实现。