一、概述
执行引擎是Java虚拟机最核心的组成部分之一。在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出是执行结果。
二、运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。典型的栈帧结构如图8-1。
1、局部变量表
局部变量表(Local Variable Table)是一组变量存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为class文件时,就在方法的Code属性的max_locals数据项中确定该方法所需要分配的局部变量表的最大值。
局部变量表的容量是以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有向导性的说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。
2、操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个先入后出(Last In First Out,LIFO)栈。操作数栈的最大深度在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64为数据类型所占的栈容量为2.在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
3、动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池的中指向方法的符号作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分为动态链接。
4、方法返回地址
执行方法,退出方式有两种,一种叫正常完成出口(Normal Method Invocation Completion)。另一种叫异常完成出口(Abrupt Method Invocation Completion)。不管什么方法退出,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复他的上层方法的执行状态。
5、附加信息
一般会把动态链接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
三、方法调用
1、解析
方法在程序真正运行之前就有一个班可确定的调用版本,并且这个方法的调用版本在运行期是不可变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。解析调用一定是个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用。
2、分派
分派(Dispatch)调用可能是静态的也可能是动态的。分派可以分为单分派和多分派。两两组合可以有静态单分派、动态单分派、静态多分派和动态多分派四中情况。
1)、静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。
2)、动态分派
运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
3)、单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则根据多余一个宗量对目标方法进行选择。
4)、虚拟机动态分派的实现
最常用的是“稳定优化”手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为itable),使用虚方法表索引来代替元数据查找以提高性能。
图8-3 方法表结构
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表的地址将会替换为指向子类实现版本的入口地址。
虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Ccahe)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)的技术守护内联(Guarded Inlining)两种非稳定的“激进优化”手段来获得更高的性能。
四、基于栈的字节码解释执行引擎
1、解释执行
Java虚拟机遵循的是基于现代经典编译原理的思路,在执行前先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树(Abstract Syntax Tree,AST)。Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成先性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机内部,所以Java程序的编译就是半独立实现的。图8-4 编译过程
2、基于栈的指令集与基于寄存器的指令集
Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。另外还有一套常用的基于寄存器的指令集,最典型的就是x86的二地址指令集,这些指令依赖寄存器进行工作。
基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。栈架构的指令集还有一些其他的优点,如代码相对更加紧凑、编译器实现更加简单等。
栈架构指令集的主要缺点是执行速度相对来说会稍微慢一些。
3、基于栈的解释器执行过程
本文来自于《深入Java虚拟机-JVM高级特性与最佳实践》---周志明。如果侵权,请联系作者删除。