1.概述
物理机的执行引擎是直接建立在处理器,缓存,指令集和操作系统层面上的.
虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件的制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式.
不同虚拟机实现中,执行引擎在执行字节码指令时,通常会有解释执行和编译执行两种选择.
执行引擎输入,输出一致的:输入是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行过程.
2.运行时栈帧结构
"栈帧"是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素.
每一个栈帧中都包括:局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息.
1.局部表量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量.
局部变量表的容量以变量槽为最小单位.其中对于32位物理内存存放数据每个变量槽存放一个boolean,byte,char,short,int,float,reference或returnAddress类型的数据.对于64位的数据类型,Java虚拟机会以高位对其的方式为其分配两个连续的变量槽空间.Java中64位的数据类型只有long和double两种.
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递.如果执行的是实例方法,那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字
"this"来访问到这个隐含参数.
局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用.
2.操作数栈
操作栈,它是一个后入先出栈.Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值.
大多虚拟机的实现中会对两个不同栈帧进行优化,令两个栈桢出现一部分重叠.让下面的栈帧部分操作数栈与上面栈帧的部分局部变量表重叠在一起,节约了一些空间,在进行方法调用时就可以直接共用一部分数据,无需进行额外的参数复制传递.
3.动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接.
Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数.这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析.另外一部分将在每一次运行期间都转化为直接引用,这部分就成为动态链接.
4.方法返回地址
两种退出方式:
1.执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方式为"正常退出".
2.在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理.无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,称为"异常调用完成".
退出后执行的操作:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等.
5.附加信息
增加一些规范里没有描述的信息到栈帧之中.
3.方法调用
方法调用不等同于方法的执行,方法调用的唯一任务是确定被调用方法的版本.
1.解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的.调用目标在程序代码写好,编译器进行编译那一刻就已经确定下来,这类方法的调用被称为解析.
在Java语言符合"编译期可知,运行期不可变"这个要求的方法.主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析.
静态方法,私有方法,实例构造器,父类方法,final修饰的方法,会在类加载的时候就可以把符号引用解析为该方法的直接引用.这些方法统称为"非虚方法".
2.分派
-
静态分派
方法重载. -
动态分派
方法重写 -
单分派和多分派
方法的接收者与方法的参数统称为方法的宗量.根据分派基于多少种宗量,可以将分派划分为单分派和多分派.单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择.现如今的Java语言是一门静态多分配,动态单分配的语言.
-
虚拟机动态分配的实现
动态分配是执行非常频繁的动作,而且动态分配的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据.
产生一直用优化手段是为类型在方法区中建立一个虚方法表.相对应的接口方法表.
为了程序实现方便,具有相同签名的方法,在父类,子类的虚方法表中都应当具有一样的索引序号,这样当类型转变时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址.虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕.
4.基于栈的字节码解释执行引擎
1.解释执行
下面的分支是传统编译原理中程序代码到目标机器代码的生成过程;
中间的那条分支,解释执行的过程.
其中从开始到指令流过程是在Java虚拟机之外进行的,而解释器在虚拟机内部,所以Java程序的编译就是半独立的实现.
2.基于栈的指令集与寄存器的指令集
Javac编译器输出的字节码指令流,基本上是一种基于栈的指令集架构,字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作.与之相对应的另一套常用的指令集架构是基于寄存器的指令集,最典型的就是x86的二进制指令集.
基于栈的指令集主要优点是可移植,因为寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束;代码相对更加紧凑;编译器实现更加简单等.
栈架构的主要缺点是理论上执行速度相对来说会稍慢一些,所有主流物理机的指令集都是寄存器架构也从侧面印证了这一点.