虚拟机字节码执行引擎

来源:互联网 发布:社会心理学 知乎 编辑:程序博客网 时间:2024/05/17 08:56

1、概述

        执行引擎是Java虚拟机最核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行的能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的系统结构,并且能够执行那么不被硬件直接支持的指令集格式。

        在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至可能包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

2、运行时栈帧结构

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

        在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

        对于执行引擎来说,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法成为当前方法。

        2.1 局部变量表

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

        虚拟机通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表的最大Slot数量。

        在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的。如果执行的是实例方法,那么第0位索引的Slot默认是用于传递方法所属对象实例的引用的,在方法中可以通过this关键字来访问道这个隐含的参数。

        类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。

        2.2 操作数栈

        也成为操作栈,它是一个后入先出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到了Code属性的max_stacks数据项中。

在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但大多数虚拟机的实现都会做一些优化处理,令两个栈帧出现一部分重叠。


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

        2.3 动态链接

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

        2.4 方法返回地址

        当一个方法开始执行后,只有两种方法可以退出这个方案。1、执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式成为正常完成出口。2、在执行过程中遇到了一场,并且这个异常没有在方法体内得到处理,,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给他的调用者产生任何返回值的。

        方法返回时可能需要在栈帧中保存一些信息,用于恢复它的上层方法的执行状态。

3、方法调用

        方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时不涉及方法内部的具体运行过程。

        3.1 解析

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

        Java中符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类。

        只有被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法。他们在类加载的时候就会吧符号引用解析为该方法的直接引用。这些方法成为非虚方法(包括final修饰的方法),与之相反,其他方法成为虚方法。

解析调用一定是一个静态的过程。而分派调用可能是静态也可能是动态的。

        3.2 分派

1、静态分派

     Human man = new Man();
        代码中Human成为变量的静态类型,“Man”成为变量的实际类型。静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅是在使用时繁盛,变量本身的静态类型是不会被改变,并且最终的静态类型实在编译期可知的;而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码:

//实际类型变化Human man = new Man();man = new Man();//静态类型变化sr.sayHello((Man) man );sr.sayHello((Woman) man );
Human man = new Man();Human woman = new Woman();sr.sayHello(man);sr.sayHello(Woman);
        两次sayHello的调用,使用哪个重载版本,就完全取决于传入参数的数量和类型。虚拟机在重载是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到方法里面的两条invokevirtual指令的参数中。

        所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。静态分派的典型应该是方法重载。静态分派发生在编译阶段,所以静态分派不是由虚拟机执行的。

        2、动态分派

        它和多态的另外一个重要——重写有着密切的关系。

        由于invokevirtual指令执行的第一步就是在运行期间确定接受者的实际类型,所以两次调用中的invokevirtual指令把常量池的类方法符号引用解析到了不用的直接引用上,这个过程就是Java方法重写的本质。我们把这种运行期根据实际类型确定方法执行版本的分配过程称为动态分派。

      3、单分派和多分派

        方法的接受者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择的。多分派是根据多余一个宗量对目标方法进行选择。



        首先看一下编译阶段编译器的选择过程,也就是静态分配过程。这时选择目标方法的依据有两点:一是静态的类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令分别为常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量选择的,所以Java语言的静态分派属于多分派类型。

        然后看看编译阶段,也就是动态分派阶段。在执行son.hardchoice(new QQ())这句代码时,由于编译阶段已经决定目标方法的签名必须为hardChoice(QQ),因此虚拟机不关心传来的参数是QQ还是腾讯QQ,因为此时参数的静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响选择的因素只有此方法接受者的实际类型是Fathe还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

      4、虚拟机动态分派的实现

        最常用的手段就是为类的方法中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。


      虚方法表中存放着各个方法的实际入口地址。

      方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

      除了方法表的方法外,在条件允许的情况下,还有使用内联缓存和基于“类型继承关系分析”技术的守护内联两种非稳定的“激进优化”手段获得更高的性能。

      3.3 动态类型语言支持

      动态语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期(PHP、Python)。相对的,在编译期就进行类型检查过程的语言(Java、C++)就是最常用的静态语言。

      动态语言的一个特点:变量无类型而变量值才有类型。

      JDK1.7中添加了java.lang.invoke包,并提供了一种新的动态确定目标方法的机制,称为MethodHandle。


4、基于栈的字节码解释执行引擎

      虚拟机如何调用方法已经解释完毕,下面介绍一下如何执行方法中的字节码指令。许多Java虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)。

      4.1 解释执行

      4.2 基于栈的指令集与基于寄存器的指令集

      Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之对应的另一套常用的指令集架构是基于寄存器的指令集。

      4.3 基于栈的解释器执行过程




























0 0
原创粉丝点击