虚拟机字节码执行引擎

2020-02-21  本文已影响0人  JBryan

执行引擎是Java虚拟机最核心的部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器,硬件,指令集和操作系统层面,而虚拟机的执行引擎则是由自己实现的。输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的,虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应者一个栈帧在虚拟机里,从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈,都已经完全确定了,并且写入到放发表的Code属性中,因此一个栈帧需要分配多少内存,仅仅取决于具体的虚拟机实现。
对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法,称为当前方法(Current Method)。

8-栈帧结构.jpg
局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中,确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean,byte,char,short,int,float,reference和returnAddress8中类型。对于64位的数据,虚拟机会以高位对齐的方式,为其分配两个连续的Slot空间,如long和double。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是,从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了第n个Slot;如果是64位数据类型的变量,则会同时使用n和n+1两个Slot。
在方法执行时,局部变量表中第0位索引的Slot是用于,传递方法所属对象实例的引用,在方法中可以通过this来访问到这个隐含的参数,其余参数,则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完成后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值,已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
操作数栈
操作数栈(Operand Stack)也通常称为操作栈,它是一个后入先出(Last In Fitst Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候,写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度,都不会超过max_stacks。
当方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令,往操作数栈中写入和提取内容,也就是出栈/入栈操作。
举个例子,整数加法的字节码指令iadd在运行的时候,操作数栈中最接近栈顶的两个元素已经存入了两个int型的值,当执行这条指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
操作数栈中元素的数据类型,必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中,还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整数加法,在执行时,最接近栈顶的两个元素的数据类型必须为int型。不能出现一个long和一个float使用iadd命令相加的情况。
在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的,但在大多数虚拟机实现里面,会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈,与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时,就可以公用一部分数据,无需额外的参数复制传递。
8-栈帧间数据共享.jpg
Java虚拟机的解释执行引擎,称为“基于栈的执行引擎”,其中所致的栈,就是操作数栈。
动态连接
每个栈帧都包含一个指向运行时常量池中,该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。字节码中的方法调用指令,就以常量池中,指向方法的符号引用,作为参数。这些符号引用,一部分会在类加载阶段,或者第一次使用的时候,就转化为直接引用,这种转化称为静态解析。另外一部分将会在每一次运行期间,转化为直接引用,这部分称为动态连接。
方法返回地址
当一个方法开始执行后,只有两种方法可以退出这个方法。
第一种是,执行引擎遇到任意一个方法返回的字节码指令,这个时候可能会有返回值,传递给上层的方法调用者。是否有返回值和返回值的类型,将根据遇到何种返回指令来决定,这种退出方法的方式称为正常完成出口。
另一种退出方法是,在执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的调用者任何返回值的。
无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时,可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来决定的,栈帧中一般不会保存这部分信息。
方法退出的过程,实际上就等同于把当前栈帧出栈,因此退出时,可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值,以指向方法调用指令后面的一条指令等。
附加信息
虚拟机规范允许具体的虚拟机实现,增加一些规范里没有描述的信息,到栈帧中。例如与调试相关的信息,这部分信息,完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址和其他附加信息归为一类,称为栈帧信息。
方法调用

方法调用并不等同与方法执行,方法调用阶段唯一的任务,是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
解析
方法调用中的目标方法,在Class文件里面通常都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用,转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前,就有一个可确定的调用版本,并且这个方法的调用版本,在运行期是不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译时,就必须确定下来。这类方法的调用,称为解析(Resolution)。
在Java语言中,符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,这两种方法各自的特点决定了他们都不可能通过继承或别的方式重写其他版本,因此他们都适合在类加载阶段进行解析。
与之相对应的是,在Java虚拟机里面提供了5条方法调用字节指令:
invokestatic:调用静态方法。
invokeespecial:调用实例构造器<init>方法,私有方法和父类方法。
invokevirtual:调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时,再确定一个实现此接口的对象。
invokedynamic:先在运行时,动态解析出,调用点限定符号引用的方法,然后再执行该方法。前面4条指令,分派逻辑是固化在虚拟机内部的,而invokedynamic的分派逻辑,是由用户所设定的引导方法决定的。
只要能被invokestatic和invokeespecial调用的方法,都可以在解析阶段中,确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器和父类方法,他们在类加载的时候,就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法,与之相反,其他方法称为虚方法。
Java中的非虚方法,除了使用invokestatic和invokeespecial之外,还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择。Java语言规范中,明确说明了final方法是一种非虚方法。
解析调用一定是一个静态的过程,在编译期间就能完全确定,在类装载的解析阶段,就会把涉及的符号引用,全部转变为可确定的直接引用,不会延迟到运行期再去完成。
分派
分派调用过程,将会揭示动态特性的一些最基本的体现,如“重载”和“重写”,在Java虚拟机中是如何实现的。
1.静态分派
代码演示:

package com.ljessie.jvm;

public class StaticDispatch {
    static abstract class Human{}
    static class Man extends Human{}
    static class Woman extends Human{}
    public static void sayHello(Human gay){
        System.out.println("Hello gay!");
    }
    public void sayHello(Man man){
        System.out.println("Hello man!");
    }
    public void sayHello(Woman woman){
        System.out.println("Hello woman!");
    }
    
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);
        sd.sayHello(woman);
    }

}

运行结果:

Hello gay!
Hello gay!

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

//实际类型变化
        Human man = new Man();
        man = new Woman();
        //静态类型变化
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello((Man) man);
        sd.sayHello((Woman) man);

编译器在重载时,是通过参数的静态类型而不是实际类型作为判断依据的,并且静态类型是编译器可知的,因此编译阶段,Javac编译器会根据参数的静态类型来决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用,写到main()里的两条invokevirtual指令的参数中。
所有依赖静态类型,来定位方法执行版本的分派动作,称为静态分派。静态分派的典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作,实际上不是有虚拟机来执行的。
2.动态分派
动态分派代码演示:

package com.ljessie.jvm;

public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }
    static class Man extends Human{

        protected void sayHello() {
            System.out.println("Man say hello");
        }
    }
    static class Woman extends Human{
        protected void sayHello() {
            System.out.println("Women say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        
        man.sayHello();
        woman.sayHello();
        
        man = new Woman();
        man.sayHello();
    }

}

运行结果:

Man say hello
Women say hello
Women say hello

使用Javap命令,输出main()的字节码

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/ljessie/jvm/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method com/ljessie/jvm/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class com/ljessie/jvm/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method com/ljessie/jvm/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method com/ljessie/jvm/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method com/ljessie/jvm/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class com/ljessie/jvm/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method com/ljessie/jvm/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method com/ljessie/jvm/DynamicDispatch$Human.sayHello:()V
        36: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 23: 16
        line 24: 20
        line 26: 24
        line 27: 32
        line 28: 36
}

16,20两句分别把刚刚创建的两个对象的引用,压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21句是方法调用指令,这两条指令但从字节码角度来看,是完全一样的,但是这两句指令最终执行的目标方法并不相同。原因就需要从invokevirtual指令的多态查找说起,invokevirtual指令的运行时解析过程大致可分为以下几个步骤:
1.找到操作数栈顶的第一个元素,所指向的对象的实际类型C
2.如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过,则返回这个方法的直接引用,查找过程结束;如果不通过,则抛出IllegalAccessError。
3.否则,按照继承关系,从下往上,依次对C的各个父类,进行第2步的搜索和验证过程。
4.如果始终没有找到合适的方法,则抛出AbstractMethodError异常。
由于invokevirtual指令执行的第一步,就是确定在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令,把常量池中的类方法符号引用,解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期间,根据实际类型,确定方法执行版本的分派过程,称为动态分派。
3.单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量,对目标方法进行选择。
单分派与多分派代码演示:

package com.ljessie.jvm;

public class Dispatch {
    static class QQ{}
    static class _360{}
    public static class Father{
        public void hardChoice(QQ arg){
            System.out.println("father choose QQ");
        }
        public void hardChoice(_360 arg){
            System.out.println("father choose 360");
        }
    }
    public static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("son choose QQ");
        }
        public void hardChoice(_360 arg){
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }

}

运行结果:

father choose 360
son choose QQ

在main()中,调用了两次hardChoice()方法。
编译阶段编译器的选择过程,也就是静态分派的过程,这时选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为,常量池中指向Father.hardChoice(360)和Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以Java的静态分派属于多分派类型。
运行阶段虚拟机的选择,也就是动态分派过程。在执行son.hardChoice(new QQ());时,由于编译器已经决定目标方法的签名必须为hardChoice(QQ),唯一可以影响虚拟机选择因素的影响是,此方法的接收者的实际类型是Father还是Son,因为只有一个宗量作为选择依据,所以Java的动态分派属于单分派类型。
虚拟机动态分派实现
最常用的手段是为类在方法区中建立一个虚方法表(Virtual Method Table),使用虚方法表索引来代替元数据查找以提高性能。上面代码的虚方法表结构如图所示。

8-虚方法表.jpg
虚方法表中存着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口,和父类相同方法的地址入口是一致的,都指向父类的地址入口。如果子类中重写了这个方法,子类方法表中的地址,将会替换为指向子类实现版本的入口地址。图中,Son重写了来自父类的所有方法,因此Son的方法表中没有指向Father类型的箭头,但是Son和Father都没有重写来自Object的方法,所以他们方法表中从Object继承来的方法,都指向了Object数据类型。
为了程序实现上的方便,具有相同签名的方法,在父类,子类的虚方法表中应当具有一样的索引序号,这样当类型转换时,仅需要变更需要查找的方法表,就可以从不同的虚方法表中,按照索引转换出所需要的入口地址。
放发表一般在类加载的连接阶段进行初始化,准备了类的变量的初始值后,虚拟机会把该类的方法表也初始完毕。
动态类型语言支持
1.动态类型语言
动态类型语言的关键特征是,它的类型检查的主体过程是在运行期而不是编译期。相对的,在编译期就进行类型检查过程的语言,就是常用的静态类型语言。
例如代码:
int[][][] array = new int[1][0][-1];

这行代码能够正常编译,但运行时会报NegativeArraySizeException异常。运行时异常就是,只要代码不运行到这一行,就不会有问题。与运行时异常相对应的就是连接时异常,例如NoClassDefFoundError,即使连接时异常的代码,放在无法执行的分支上,类加载时(Java的连接过程在类加载阶段),也会抛出异常。
Java语言在编译期已经将方法的符号引用生成出来,作为方法调用指令的参数,存储到Class文件中,例如下面:

invokevirtual #6                  // Method com/ljessie/jvm/DynamicDispatch$Human.sayHello:()V

这个符号引用,包含了此方法定义在哪个具体类型中,方法的名字及以及参数顺序,参数类型和方法返回值等信息,通过这个符号引用,虚拟机可以翻译出这个方法的直接引用。
静态类型语言,在编译器确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题,在编译期就能被发现,利于稳定性及代码达到更大规模。而动态类型语言,在运行期间确定类型,这可以为开发人员提供更大的灵活性,某些在静态类型语言中,需要大量“臃肿”代码才能实现的功能,由动态类型语言来实现可能会更加清晰简洁,清晰和简洁通常也意味着开发效率的提升。
2.java.lang.invoker包
这个包的主要目的是在之前,单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle。
MethodHandle演示:

package com.ljessie.jvm;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleTest {

    static class ClassA{
        public void println(String s){
            System.out.println(s);
        }
    }

    private static MethodHandle getPrintlnMethodHandle(Object receiver) throws NoSuchMethodException, IllegalAccessException {
        /**
         * MethodType代表方法类型,包含了返回值(第一个参数),和具体参数(第二个参数)
         */
        MethodType methodType = MethodType.methodType(void.class,String.class);

        /**
         * lookup():在指定类中,查找符合给定的方法名称,方法类型,并且符合调用权限的方法句柄
         */
        return MethodHandles.lookup().findVirtual(receiver.getClass(),"println",methodType).bindTo(receiver);
    }

    public static void main(String[] args) throws Throwable {
        Object obj = new ClassA();
        Object obj2 = System.out;
        /**
         * 无论obj是哪个实现类,都能正确调用到println()
         */
        getPrintlnMethodHandle(obj).invokeExact("obj invoke");
        getPrintlnMethodHandle(obj2).invokeExact("obj2 invoke");
    }
}

运行结果:

obj invoke
obj2 invoke

实际上,方法getPrintlnMethodHandle()中模拟了invokevirtual指令的调用过程,只不过它的分派逻辑,是通过一个方法来实现,而这个方法的返回值(MethodHandle),可以视为对最终调用方法的一个引用。以此为基础,就可以定义下面方法:

void sort(List list ,MethodHandle compare){};

从本质上讲,Reflection和MethodHandle机制都是在模拟方法调用,但Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。MethodHandles.lookup()中的三个方法,findStatic(),findVirtual(),findSpecial()正是为了对应invokestatic,invokevirtual,invokespecial,invokespecial这几条字节码指令,执行权限校验行为。而这些底层细节,在使用Reflection API时,是不需要关心的。
Reflection中的Method对象,比MethodHandle对象包含的信息多,Method是Java一端的全面映像,包含了方法的签名,描述符,以及方法属性表中各属性的Java端表达式,还包含执行权限等运行时信息,而后者仅仅包含与执行该方法相关的信息。Reflection是重量级的,MethodHandle是轻量级的。
Reflection API的设计目标是为Java语言服务的,而MethodHandle则设计成可服务于所有Java虚拟机之上的语言,其中也包括Java语言。
3.invokedynamic指令
invokedynamic指令与MethodHandle机制的作用一样,都是为了解决原有4条invoke*指令方法分派规则固化在虚拟机里的问题,把如何查找目标方法的决定权,从虚拟机转嫁到具体的用户代码之中,让用户由更高的自由度。而且,两者的思路也是可类比的,一个采用上层Java代码和API实现,另一个用字节码和Class中其他属性,常量来完成。
每一处含有invokedynamic指令的位置,都称为动态调用点。这条指令的第一个参数是CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到三个信息:引导方法,方法类型和名称。

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

Java虚拟机的执行引擎,在执行Java代码的时候,有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,先讨论解释执行。
解释执行
大部分的程序代码到物理机的目标代码,或虚拟机能执行的指令集之前,都要经过图中的各个步骤。下面那条分支,就是传统编译原理中,程序代码到目标机器代码的生成过程,而中间的那条分支,就是解释执行的过程。

8-编译过程.jpg
Javac编译器完成了程序代码经过词法分析,语法分析到抽象语法树,再遍历语法树,生成线性的字节码指令流的过程,是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。
基于栈的指令集与基于寄存器的指令集
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令,大部分都是零地址指令,他们依赖操作数栈进行操作。与之相对应的另外一套常用的指令集架构,就是现在主流PC中直接支持的指令集架构,依赖寄存器进行工作。
分别用两种指令集,计算1+1的结果,基于栈的指令集会是这样的:
iconst_1
iconst_1
iadd
istore_0

两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈,相加,然后把结果放回栈顶,最后istore_0把栈顶的值,放到局部变量表的第0个Slot中。
如果基于寄存器,那程序可能会是这个样子:

mov eax.1
add eax.1

mov指令把eax寄存器的值设为1,然后add指令,再把这个值加1,结果就保存在eax寄存器里面。
基于栈的指令集主要的有点是可移植,寄存器由硬件直接提供,程序依赖这些硬件寄存器,则不可避免的要收到硬件的约束。栈架构的指令集还有其他优点,如代码相对紧凑,编译器实现更加简单。栈架构的缺点是执行速度稍微慢一些。
基于栈的解释器执行过程
示例代码:

public int calc(){
        int a = 100;
        int b = 200;
        int c = 300;
        return (a+b)*c;
    }

使用javap查看它的字节码指令

public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 7
        line 8: 11
}

javap这段代码需要深度为2的操作数栈,和4个Slot的局部变量空间,下面几张图来描述代码执行过程中的代码,操作数栈和局部变量表的变化情况。


8-执行指令0.jpg
8-执行指令1.jpg
8-执行指令11.jpg
8-执行指令12.jpg
8-执行指令13.jpg
8-执行指令14.jpg
8-执行指令16.jpg

上面的执行过程,仅仅是一种概念模型,虚拟机最终会对执行过程来做一些优化来提高性能。实际情况会和上面描述的概念模型差距非常大,这种差距产生的原因是,虚拟机中解析器和即时编译器都会对输入的字节码进行优化。

上一篇 下一篇

猜你喜欢

热点阅读