【JVM】虚拟机字节码执行引擎

2017-07-23  本文已影响59人  maxwellyue

Java虚拟机的执行引擎:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本章主要是从概念模型的角度讲解虚拟机的方法调用和字节码执行。


帧栈

帧栈概念

帧栈是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

每一个帧栈中都包括以下信息:局部变量表(Local Varable Table)、操作数栈(Operand Stack)、动态连接(Dynamic Linking)、方法返回地址(Return Address)和一些额外的附加信息。

一个帧栈需要分配多大内存,不会受到程序运行期间变量数据的影响,而是在程序代码编译时就确定了的(在方法表的Code属性中,详见类文件结构中的内容)。

一个线程中的方法调用链可能会很长,对于执行引擎来说,在活动线程中,只有位于栈顶的帧栈才是有效的,称为当前帧栈,与这个帧栈相关联的方法称为当前方法。执行引擎运行的字节码指令都只针对当前帧栈进行操作,在概念模型上,典型的帧栈结构如下(栈是线程私有的,也就是每个线程都会有自己的栈)。

典型的帧栈结构
//--------------------------测试1---------------------------//
public static void main(String[] args){
        byte[] placeholder = new byte[64*1000*1000];
        System.gc();
}
//查看日志,并未回收
[GC (System.gc())  69437K->63438K(251392K), 0.0012879 secs]
[Full GC (System.gc())  63438K->63277K(251392K), 0.0058505 secs]
//------------------------测试2-----------------------------//
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1000 * 1000];
        }
        System.gc();
}
//查看日志,并未回收
[GC (System.gc())  69437K->63420K(251392K), 0.0011785 secs]
[Full GC (System.gc())  63420K->63277K(251392K), 0.0058676 secs]
//------------------------测试3-----------------------------//
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1000 * 1000];
        }
        int a = 0;
        System.gc();
}
//查看日志,回收了
[GC (System.gc())  69437K->63454K(251392K), 0.0011921 secs]
[Full GC (System.gc())  63454K->777K(251392K), 0.0056915 secs]

测试1中在System.gc()时,变量placeholder还处在作用于之内,不会回收;测试2在System.gc()时,变量placeholder虽然已经不在作用域,但是placeholder原本所占用的Slot还没有被复用,所以作为GC Root一部分的局部变量表仍然保持着对它的关联,所以也没有回收。这种关联没有被及时打破的影响在绝大部分
下都很轻微,但假如有一个方法,后面的代码有一些耗时很长的操作,而前面又定义了占用大量内存、实际已经不会再使用的变量,则手动将其设为null是有意义的。</br>
还有一点就是,局部变量不像类变量(仅指被static修饰的变量,不包括实例变量)一样存在准备阶段,它不存在系统默认值。所以必须为局部变量定义初始值。(不指定,编译也会报错)。


方法调用

方法调用并不等同于方法执行,方法调用阶段的唯一目的就是确定被调用的方法的版本(即调用哪个方法)。一切方法调用在Class文件里存储的都是符号引用,而不是方法的直接引用(方法在实际运行时内存布局中的入口地址)。

在虚拟机中,有5条方法调用字节码指令:
invokestatic:调用静态方法;
invokespecial:调用实例构造器<init>方法、私有方法和父类方法;
invokevirtual:调用所有的虚方法;
invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象;
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

方法的调用可以分为解析调用和分派调用。

Human human = new Man();//这里假设Man是Human的子类

上述代码中:Human为变量的静态类型,Man为变量的实际类型。
虚拟机(准确地说是编译器)在重载时,是通过参数的静态类型而不是实际类型作为判定依据的。

public class StaticDispatch {

    static abstract class Human{}

    static class Man extends Human{}

    static class Woman extends Human{}

    public void sayHello(Human human){
        System.out.println("hello, human");
    }

    public void sayHello(Man man){
        System.out.println("hello, man");
    }

    public void sayHello(Woman woman){
        System.out.println("hello, woman");
    }

    @Test
    public void test(){
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch dispatch = new StaticDispatch();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
    }
}
//最终的打印结果如下:(重载时是以静态类型判断的)
hello, human
hello, human

编译器虽然可以确定方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。这种情况的产生的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推测。

public class OverLoad {

        public static void sayHello(Object object){
            System.out.println("hello Object");
        }
        public static void sayHello(int a){
            System.out.println("hello int");
        }
        public static void sayHello(long a){
            System.out.println("hello long");
        }
        public static void sayHello(Character character){
            System.out.println("hello Character");
        }
        public static void sayHello(char c){
            System.out.println("hello char");
        }
        public static void sayHello(char... c){
            System.out.println("hello char...");
        }
        public static void sayHello(Serializable serializable){
            System.out.println("hello Serializable");
        }

        @Test
        public void test(){
            sayHello('a');
        }
}

以上代码,将打印出hello char
①将sayHello(char c)注释掉,将打印出:hello int
②继续将sayHello(int a)注释掉,将打印出:hello long
这两步的原因是字符a发生自动类型转换(char->int->long->float->double)
③继续将sayHello(long a)注释掉,将打印出:hello Character
原因是字符a被自动装箱为Character类型
④继续将sayHello(Character character)注释掉,将打印出:hello Serializable
原因是a被自动装箱为Character类型后仍然找不到方法,继续自动转型,Character实现了Serializable接口。
⑤继续将sayHello(Serializable serializable)注释掉,将打印出:hello Object
原因是char装箱后转型为父类了,如果有多个父类,将在继承关系中从下往上搜索,约接近上层优先级越低。
⑥继续将sayHello(Object object)注释掉,将打印出:hello char...
可见:可变长参数的重载优先级是最低的。

public class DynamicDispatch {

        static abstract class Human {
            abstract void sayHello();
        }

        static class Man extends Human {
            @Override
            void sayHello() {
                System.out.println("man say hello");
            }
        }

        static class Woman extends Human {
            @Override
            void sayHello() {
                System.out.println("woman say hello");
            }
        }

        @Test
        public void test() {
            Human man = new Man();
            Human woman = new Woman();
            man.sayHello();
            woman.sayHello();
        }
}
//将会打印
man say hello
woman say hello

通过以上可知,静态分派与动态分派是不同情况下方法调用所采取的不同的分派方式,两者并不是非此即彼的,还可能出现一个方法调用在确定直接引用时,既用到静态分派,又用到动态分派。确定重载方法的时候用到的是静态分派,确定重写方法的时候用到的是动态分派。即重载看参数静态类型,重写看参数实际类型。这里的参数,重载时是指方法的参数列表中那个参数,重写时是指该方法的调用者。


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

主要探讨虚拟机如何执行方法中的字节码指令。许多Java虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器生成本地代码执行)两种选择,这里进讨论解释执行。

解释执行
基于栈的指令集和基于寄存器的指令集
基于栈的解释器执行过程
上一篇 下一篇

猜你喜欢

热点阅读