一、概述
执行引擎是java虚拟机最核心的组成之一,与物理机最大的区别就是它是由虚拟机自己实现的,因此可以自行指定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。
二、运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法调用开始至执行完成的过程都是对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态,对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,执行引擎运行的所有字节码指令都只针对于当前栈帧进行操作。
1.局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,但是虚拟机规范中并没有指定这个最小单位的内存空间大小,一个Slot可以存放一个32位以内的数据类型,java虚拟机中主要有8种:boolean、byte、char、short、int、float、reference、returnAddress。对于64位的数据类型,虚拟机会以高位对齐的方式为其连续分配两个连续的Slot空间,java语言中明确的64类型的只有long和double两种。其实虚拟机为了节省栈帧空间,局部变量表中的Slot是可以重用的。
2.操作数栈:也称为操作栈,是一个后入先出的栈,同局部变量表一样,操作数栈的最大深度也是在编译的时候写入到Code属性的max_stacks数据项中,操作数栈的每一个元素可以是任意的java数据类型,包括long和double,32位数据类型所占用的栈容量是1,64位的就是2。
3.动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法放入引用,持有这个引用是为了支持方法调用过程中的动态连接。
4.方法返回地址:当一个方法执行后只有两种方式可以退出这个方法,第一种是执行引擎遇到任意一个返回的字节码指令,另外一种就是在方法的执行过程中遇到了异常,并且这个异常没有在方法体内得到处理。两种方法退出之后都需要返回到方法调用的位置,程序才能继续执行。
5.附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中。
三、方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。
1.解析:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的,换句话说,调用目标在程序代码写好,编译器进行编译时就必须确定下来,这类方法的调用称为解析。其实在java语言中符合“编译器可知,运行期不可变”规范的主要是静态方法和私有方法,在java虚拟机中提供了5条方法调用字节码指令:
- invokestatic 调用静态方法
- invokespecial 调用实例构造器<init>方法,私有方法和父类方法
- invokevirtual 调用所有的虚方法(类加载的时候不会把符号引用解析为该方法的直接引用)
- invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
- invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法,是有用户所设定的引导方法决定,前面四种都是固化在虚拟机内部的
2.分派
- 静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载
- 动态分派:在运行期根据实际类型确定方法执行版本的分派过程称为动态分派,动态分派的典型应用是方法重写
- 单分派和多分派:方法的接受者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派,单分派是根据一个宗量对目标进行选择,多分派则是根据多于一个宗量对目标进行选择。其中java语言的静态分派属于多分派类型,动态分派属于单分派类型。
- 虚拟机动态分派的实现:由于动态分派是非常频繁的动作,所以动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因为虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索,面对这种情况,最常用的稳定化手段就是在类的方法区中建立一个虚方法表,使用虚方法索引表来代替元数据查找以提高性能。
3.动态类型语言支持
- 动态类型语言:动态类型语言的特征是类型检查的主体过程是在运行期而不是编译期,满足这个特征的语言很多,如APL、Clojure、Groovy等,相对的,在编译期就对类型检查过程的语言(C++、Java)就是常用的静态类型语言。其实静态类型语言和动态类型语言都有各自的优点,静态的在编译的时候就能提供严谨的检查,及时发现利于稳定,动态类型语言可以为开发者提供更大的灵活性,避免臃肿代码,实现更加简洁,效率更高。
- JDK1.7与动态类型:在这之前java是不支持动态类型语言的,所以只能借助其他的方式来实现,但是JDK1.7中增加invokedynamic指令以及java.lang.invoke包。
- java.lang.invoke包就是JSR-292的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法放入机制,称为MethodHandle。这种设计可服务于所有java虚拟机之上的语言,其中也包含java语言。
- invokedynamic指令从某种程度上来说,与 MethodHandle机制的作用相同。
- 掌控方法分派规则:前面说过invokedynamic指令的分派逻辑不是由虚拟机决定的,而是有程序员决定的。
四、基于栈的字节码解释执行引擎
1.解释执行:在java刚刚出现的时候人们把其定位为“解释执行”语言,但是当主流的虚拟机中包含了即时编译器后,又被定位为“编译执行”,所以这个现在也是说不清楚,其实只有具体的java实现版本和执行引擎运行模式时,讨论这个才有意义。其实解释执行就是为了说明机器其实是不能像人一样去思考问题,而是按照一定的规则顺序来执行。
2.基于栈的指令集与基于寄存器的指令集:前者指令流中的指令大部分都是零地址指令,依赖操作栈进行工作,后者是依赖寄存器进行工作,两者都是各有优势(去过有一方能够被取代,这里就不会讨论两个啦)。基于栈的优点是可移植,寄存器是由硬件直接提供,所以会受到硬件的约束,还有代码更加紧凑,编译器实现更加简单,缺点是执行效率会比较慢,反过来就是基于寄存器的优点和缺点。
3.基于栈的解释器执行过程:总体来说所有的中间变量都是以操作数栈的出栈、入栈为信息交换途径的。