7.虚拟机字节码执行引擎

2020-02-22  本文已影响0人  xMustang

虚拟机字节码执行引擎

执行引擎在执行Java代码时,有两种方式:解释执行、编译执行(JIT产生本地代码)。

1. 运行时栈帧结构

栈帧结构:

栈帧结构

每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

在活动线程中,只有位于栈顶的栈帧是有效的,称为当前栈帧,与当前栈帧关联的方法称为当前方法。

JVM中的虚拟机栈:

虚拟机栈

1.1 局部变量表

Local Variable Table

局部变量表是一组变量值存储空间,存放方法参数、方法内部定义的局部变量、显式异常处理器的参数(catch块中的异常e)。

Class文件的Code属性的max_locals确定了方法需要的局部变量表的最大容量。

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

局部变量表复用slot,在某些情况下会对垃圾收集有影响(局部变量表是GC Roots的一部分):

public static void main(String[] args) {
    {
        byte[] bytes = new byte[64 * 1024];
    }
    System.gc();
}
上面代码在不经过JIT编译时,bytes不能被回收。
局部变量表还存在着对bytes的引用,所以不能被回收。
public static void main(String[] args) {
    {
        byte[] bytes = new byte[64 * 1024];
    }
    int a;
    System.gc();
}
上面代码在不经过JIT编译时,bytes可以被回收。
局部变量表中bytes占用的slot被a复用,局部变量表中不存在bytes,可以被回收。

如果遇到一个方法,这个方法前面定义了占用大量内存、实际上已经不再使用的变量,方法后面代码耗时很长时,可以将全面占用大内存的变量手动设置为null,去掉局部变量表中对大内存对象的关联。如果这个方法调用次数达到JIT的编译条件,那么不需要设置为null,大内存对象也会被回收。

1.2 操作数栈(Operand Stack)

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

Class文件Code属性中的max_stacks指定了操作数栈的最大深度。

32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。

方法的执行过程中,各种字节码指令往操作数栈中写入、提取内容,也就是入栈、出栈。如,加法指令作用于操作数栈顶的两个元素,调用其他方法时,通过操作数栈进行参数传递。

在概念模型中,虚拟机栈的两个栈帧是完全独立的元素。在具体实现中,虚拟机做了优化,两个栈帧有重叠:让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时,可以共用部分数据,不需要进行额外的参数复制传递。

操作数栈

1.3 方法返回地址

两种方式退出执行中的方法:

  1. 正常完成出口

    遇到方法返回的字节码指令。是否有返回值根据遇到何种方法返回指令来决定。

  2. 异常完成出口

    方法产生的异常在异常表中没有搜到匹配的异常处理器,方法退出,此时不会给上层调用者返回任何值。

1.4 附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧中,如与调试相关的信息等。

2. 方法调用

2.1 解析

在类加载的解析阶段,会将其中的一部分方法的符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。

只要能被invokestatic、invokespecial调用的方法,都满足上面条件,包括静态方法、私有方法、实例构造器、父类方法4类。final方法被invokevirtual调用,但是也满足上面的条件。这些方法会在类加载时被解析成唯一的方法调用版本。

2.2 分派

  1. 静态分派

    与重载有关。

    public static void say(Number n) {
        System.out.println("number");
    }
    
    public static void say(Integer i) {
        System.out.println("integer");
    }
    
    public static void main(String[] args) {
        Number number = new Integer(2);
        say(number);
    }
    // 运行结果:
    number
    

    对象有静态类型、实际类型,如Number number = new Integer(2)中的number静态类型是Number,实际类型是Integer。

    在编译阶段,编译器根据参数的静态类型来决定使用方法的哪个重载版本,并且是选择一个“更加合适”的版本。

  2. 动态分派

    与重写有关。

    重写方法被调用时,字节码会生成invokevirtual指令,invokevirtual运行过程如下:

    1. 找到操作数栈顶的第一个元素指向的对象的实际类型,记作C。
    2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验:如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回javalang.IllegalAccessError
    3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索、校验过程。
    4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError。

宗量

方法的宗量包括方法的接收者、方法的参数。

Java1.7及之前是静态多分派、动态单分派的语言。

4. 附件

虚拟机栈帧结构.vsdx

上一篇下一篇

猜你喜欢

热点阅读