Java虚拟机-虚拟机字节码执行引擎
所有Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果;
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中虚拟机栈的栈元素。
栈帧中存储了局部变量表、操作数栈、动态链接和方法返回地址等信息;
每个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里面中从进栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态链接和方法返回地址和一些额外的附加信息。
在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中;
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。
与这个栈帧相关联的方法称为当前方法。
执行引擎运行的所有字节码指令都只是针对当前栈帧进行操作;
方法调用局部变量表局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件的时候,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量;局部变量表是以Slot(容量槽)为最小单位;一个Slot可以存放一个32位以内的数据类型;Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress8中数据类型;Java语言中明确的64位数据类型只有long和double两种。需要用两个连续的Slot来存放。由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的Slot数量。为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的。如果访问的是32位数据类型的变量,索引n就是代表了使用第n个Slot,如果访问的是64位数据类型变量,则说明会同时使用n和n+1两个Slot;类变量有两次赋初始值的过程,一次是在准备阶段,赋予系统初始值;另外一次是在初始化阶段,赋予程序员定义的初始值;即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。操作数栈操作数栈也常称为操作栈,它是一个后入先出栈。操作数栈的最大深度在编译的时候就写入方法表的Code属性的max_statcks数据项中;操作数栈的每一个元素可以是任意的Java数据类型,包括long和double,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。动态连接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。方法返回地址当一个方法开始执行后,只有两种方式可以退出这个方法:1、执行引擎遇到任意个方法返回的字节码指令,这种退出方法的方式称为正常完成出口。2、在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,这种退出方法的方式称为异常完成出口。
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时不涉及方法内部的具体运行过程。
解析类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。解析能成立的前提是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。调用目标在程序代码写好、编译器进行编译时就必须确定下来。在Java语言中符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类;前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析;在Java虚拟机里面提供了5条方法调用字节码指令,如下:
基于栈的字节码解释执行引擎1、invokestatic:调用静态方法;2、invokespecial:调用实例构造器<init>方法、私有方法和父类方法;3、invokevirtual:调用所有虚方法;4、invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象5、invokedynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实力构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法。分派静态分派,所有依赖静态类型来定位方法执行版本的分派动作;发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。例如重载,虚拟机在重载的时候是通过参数的静态类型而不是实例类型作为判断依据的,并且静态类型是编译器可知的。动态分派,在运行期根据实际类型确定方法执行版本的分派工作;例如重写单分派与多分派,方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派;单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行执行;动态类型语言支持动态类型语言静态类型语言:在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题能在编码的时候及时发现,利于稳定性及代码达到更大规模;动态类型语言:在运行期确定类型,这可以为开发人员提供更大的灵活性,某些在静态类型语言中需要大量臃肿代码来实现的功能,由动态类型语言来实现可以更加清晰和简洁,清晰和简洁通常也就意味着开发效率的提升。JDK1.7与动态类型目前已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等。方法符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型。这样,在Java虚拟机上实现的动态类型语言就不得不使用其他方式实现,这样势必让动态类型语言实现的复杂度增加,也可能带来额外的性能或者内存开销。java.lang.invoke包主要是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle。仅站在Java语言的角度来看,MethodHandle的使用方法和效果与Reflection有众多相似之处,不过它们还有以下区别:1、从本质上将,Reflection和MethodHandle机制都是在模拟方法调用,但Reflection是在模拟Java代码层次的方法调用,MethodHandle是在模拟字节码层次的方法调用。2、Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethosHandle对象所包含的信息多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的java端表现方式,还包含执行权限等的运行期信息。后者仅仅包含与执行该方法有关的信息。Reflection是重量级,MethodHandle是轻量级3、MethodHandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面做的各种优化Reflection API的设计目标只是为了Java语言服务的MethodHandle 则设计成可服务于所有虚拟机之上的语言,其中也包含java语言。invokedynamic指令在某种程度上,invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有4条"invoke*"指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码中,让用户有更高的自由度。
由于目前的主流虚拟机都包含了即时编译器,所以Java已经不再是纯解释执行的语言,而且Java编译器输出的指令集是基于栈的。
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。
基于栈的指令集主要的优点是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免的要受到硬件的约束。
栈架构指令集的主要缺点是执行速度相对来说会慢一些。