虚拟机字节码执行引擎

1.执行引擎是java虚拟机最核心的组成部分之一。“虚拟机”和“物理机”都有执行代码的能力,区别是物理机的执行引擎时直接建立在硬件、处理器、指令集和操作系统上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不呗硬件直接支持的指令集格式。

2.java虚拟机规范制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各种虚拟机执行引擎的统一外观,在不同的虚拟机实现里,执行引擎在执行java代码的时候可能有解释执行(用解释器)或编译执行(用编译器),或者两者兼备,甚至可能包含几个不同级别的编译器执行引擎。但是从外观上看所有的java虚拟机的执行引擎都时一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

3.栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。如图。

4.一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态,对于执行引擎来说,活动线程中,只有栈顶的栈帧事有效的,称为当前栈帧。这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

5.每个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定,并且写入到方法表Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而是仅仅取决于具体的虚拟机实现。

6.局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

7.局部变量表的容量以变量槽Variable Slot为最小单位,虚拟机规范中并没有指出一个Slot应占用的内存空间大小,只是说能存放一个32位以内的数据类型,java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress八种类型,reference是对象的引用,规范中没有说明它的长度和结构,但是至少能之间或者间接查找到对象在java堆中的起始地址索引和方法区中的对象数据类型。而returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。

对于64位的数据结构,虚拟机为其分配两个连续的Slot空间。Java中明确规定的64位数据结构只有long和double两种。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据变量则说明使用第n和第n+1两个Slot。

8.操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

                  当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。

                  操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证一点,在类校验阶段的数据流分析中还要再次验证这一点。

                  概念模型中,两个栈帧作为虚拟机的元素,相互之间是完全独立的。但是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分与上面的栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,而无须进行额外的参数复制传递了,如图所示。

                  Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

9.每个栈帧都包含一个支系那个运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接,Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析,另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。

10.当一个方法被执行后,有两种方式退出这个方法。第一种是执行引擎遇到任何一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

                  另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,制药在本方法的异常表中没有搜索到匹配的异常处理器,就睡导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

                  无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,涌来帮助恢复它的上层方法的执行状态。一般来说方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

                  方法退出的过程实际上等同于把当前栈帧出栈,因此退出时候可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值则将其压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

11.虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归位一类,称为栈帧信息。

12.方法调用不同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,即调用哪一个方法,暂时还不涉及方法内部的具体运行过程。程序运行时进行方法调用是最普遍、最频繁的操作,Class文件的编译过程中不包含传统编译的连接步骤,一切方法调用在Class文件里面存储的都时符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用),这个特性给java带来了更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

13.所有方法调用中的目标方法在Class文件里面都时一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。在类加载的解析阶段会将其中的一部分符号引用转化为直接引用,这种解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。

14.在java中符合“编译期不可变,运行期不可变”这个要求的方法主要有静态方法和私有方法两类,前者与类型直接关联,后者在外部不可被访问。这两种方法都不可能通过继承或者别的方式重写出其他版本,因此它们都适合载类加载阶段进行解析,与之对应的四条调用字节码指令,分别是:


只要是能被前两个指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类。它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法,其他方法称为虚方法(final方法除外)。

                  非虚方法除了使用invokestatic和invokespecial调用的方法之外还有一种,就是final修饰的方法,虽然final方法使用invokevirtual指令来调用的,但是由于它无法被覆盖没有其他版本,所以无需对方法接受者进行多态选择,java中明确说明了final方法是一种非虚方法。

                  解析调用一定是一个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。

15.分派调用可能是静态的也可能是动态的,根据分派的依据的宗量数可分为单分派和多分派。两两组后就构成了静态单分派、静态多分派、动态单分派、动态多分派四种情况。

16.分派调用过程将会揭示多态性特征的一些最基本的体现。

17.所有以来静态类型来定位方法执行版本的分派动作都是静态分派,典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

18.重载方法的匹配是有优先级的。

19动态分派和多态性的另一个重要体现——重写有密切关联,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

20.invokevirtual指令的运行时解析过程大致分为以下步骤:

21.方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种,单分派时根据一个宗量对目标方法进行选择,多分派是根据多于一个的宗量对目标方法进行选择。

22.编译阶段编译器的选择过程是静态分派过程,这时候选择目标方法的依据有两点:一是静态类型,二是方法参数,java语言的静态分派属于多分派类型。

                  运行阶段虚拟机的选择,即动态分派的过程,这时候影响选择目标方法的因素只有此方法的接收者的实例类型。因为只有一个宗量作为选择依据,所以java语言的动态分派属于单分派类型。

23.动态分派操作很频繁,而且会进行很频繁的目标方法搜索,最常用的稳定优化手段就是为类载方法区中建立一个虚方法表vtable,使用虚方法表索引来代替元数据查找以提高性能,如图,除此之外还可能使用内联缓存技术和守护内联两种非稳定的激进化手段来获得更好的性能。

24.虚方法表中存放着各个方法的实际入口地址。如果某个方法载子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口,如果子类中重写这个方法,子类方法表中的地址将会被替换为指向子类实现版本的入口地址。

25.为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换所需的入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类的的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

25.java虚拟机的执行引擎在执行java代码的时候都有解释执行(解释器执行)和编译执行(编译期产生本地代码执行)两种选择。Java语言到底是哪种类型的挚友虚拟机,只有在讨论对象是某种具体的java实现版本和执行引擎运行模式时才能知道。

26.大部分的程序代码到物理机的目标或者虚拟机能执行的指令集之前,都需要进行以下步骤,如图,下面的分支是传统编译原理中程序代码到目标机器代码的生成过程,而中间的那条分支即使解释执行的过程。

在执行前对源码进行词法和语法分析处理,把源码转换为抽象语法树AST,作为一门具体的元实现,把其中一部分步骤实现为一个半独立的编译器,这类代表就是java语言。

词法和语法分析乃至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译期去实现,这类代表是C/C++。

又或者把这些步骤和执行引擎都封装在一个封闭的黑盒种种,如javaScript执行期。

27.java语言中,javac编译器完成了程序代码经过词法分析、语法分析道抽象语法树,再遍历语法树生成线性的字节码指令流的过程,因为这一部分动作时在java虚拟机之外进行的,而解释器在虚拟机的内部,所以java程序的编译就是半独立的实现。

28.java编译器输出的指令流基本上是一种基于栈的指令集架构,与之相对的是一套常用的是基于寄存器的指令集。基于栈的指令集的优点是可移植性、代码更紧凑、编译实现更简单、完成更多的指令,但是速度比基于寄存器的指令集更慢。

 

你可能感兴趣的:(java,JVM)