虚拟机字节码执行引擎

  物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。所有的执行引擎的基本一致:输入:字节码文件 处理:字节码解析 输出:执行结果。

运行时栈帧结构

  栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中虚拟机栈的栈元素。每个栈帧都包括了一下几部分:局部变量表、操作数栈、动态连接、方法的返回地址和一些额外的附加信息。每一个方法从调用开始到结束的过程都对应着一个栈帧在虚拟机栈里入栈和出栈的过程。栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中。因此一个栈帧需要分配多少内存,不会受程序运行时变量数据的影响,仅仅取决于虚拟机的实现。在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。需要注意的是一个栈中能容纳的栈帧是受限,过深的方法调用可能会导致StackOverFlowError,当然,我们可以认为设置栈的大小。其模型示意图大体如下:

 局部变量表

  局部变量表是一组变量的存储空间,用于存放方法参数和方法内部定义的局部变量,其容量用Slot(变量槽)作为最小单位,一个变量槽可以存放一个32位以内的数据类型,对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法,那局部变量表第0位索引的SLot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。为了尽可能节省栈帧空间Slot可以重用。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。举例说明:

public static void main(String[] args){
        int a;
        System.out.println(a);
}
View Code

操作数栈

  操作数栈也常称为操作栈,是一个后入先出栈。同局部变量一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。栈中的任何一个元素都是可以任意的Java数据类型,包括long和double类型。32位数据类型所站的栈容量为1,64位数据类型所占的容量为2.当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数中写入和提取内容,也就是出栈/入栈操作。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。大多数虚拟机会做一些优化处理,令两个栈帧出现一部分重叠。

动态连接

  每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有该引用是为了支持方法调用过程中的动态连接。

方法的返回地址

  当一个方法开始之后,只有两种方式可以退出这个方法:1、执行引擎遇到任意一个方法返回的字节码指令,也就是所谓的 正常完成出口。2、在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导 致方法退出,这种方式成为异常完成出口。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,而通过异常退出的,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。

方法调用

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

解析

  所有方法调用中目标方法在Class文件里面都是一个常量池中的符号引用,在类解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法调用称为解析。这类的方法主要包括静态方法和私有方法两大类。只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确认唯一的调用版本,符合这些条件的有静态方法、私有方法、实例构造器、父类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法称为非虚方法,其他的方法称为虚方法(除final方法)。

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

分派(Diapatch)

  分派调用也可能是静态的也可能是动态的,根据分派依据的宗量数可以分为单分派和多分派。这两类分派组合就构成了静态单分派、静态多分派、动态单分派和动态多分派。

 1.静态分派

public class StaticDispatch {
    static class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

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

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

    public static void sayHello(Woman woman) {
        System.out.println("hello,human");
    }
    public static void main(String[] args) {
        StaticDispatch dispatch=new StaticDispatch();
        Human man=new Man();
        Human woman=new Woman();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
    }
}
View Code

   代码中Human称为变量的静态类型,Man称为动态类型。静态类型和动态类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,而变量本身的静态类型不会发生变化,并且最终的静态类型是在编译器可知的;而实际类型的变化结果是运行期才可以确定。虚拟机在重载时通过参数的静态类型而不是实际类型作为判定依据。并且静态类型是编译器可知的,因此编译阶段,javac编译器会根据参数的静态类型决定使用哪个版本。所有依赖静态类型来定位方法执行版本的分派称为静态分派。静态分派的典型应用是方法重载,发生在编译阶段,因此静态分派的动作不是由虚拟机来执行的。

public class OverLoad {
    public static void sayHello(Object object) {
        System.out.println("hello object");
    }
    public static void sayHello(int arg) {
        System.out.println("hello int");
    }
    public static void sayHello(long arg) {
        System.out.println("hello long");
    }
    public static void sayHello(Character object) {
        System.out.println("hello Character");
    }
    public static void sayHello(char object) {
        System.out.println("hello char");
    }
    public static void sayHello(char ... object) {
        System.out.println("hello char...");
    }
    public static void sayHello(Serializable object) {
        System.out.println("hello Serializable");
    }
    public static void sayHello(Comparable<?> object) {
        System.out.println("hello Comparable");
    }
    public static void main(String[] args) {
        sayHello('c');
    }
}
View Code

2.动态分派

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

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

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

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

  重载一般使用静态分配,由静态类型决定;重写使用动态分配,由动态类型决定。

3.单分派与多分派

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

public class Dispatch {
    static class QQ {
    }

    static class _360 {
    }

    public static class Father {
        public void harhChoice(QQ arg){
            System.out.println("father choose QQ");
        }
        public void harhChoice(_360 arg){
            System.out.println("father choose _360");
        }
    }
    public static class Son extends Father {
        public void harhChoice(QQ arg){
            System.out.println("son choose QQ");
        }
        public void harhChoice(_360 arg){
            System.out.println("son choose _360");
        }
    }
    public static void main(String[] args) {
        Father father=new Father();
        Father son=new Son();
        father.harhChoice(new _360());
        son.harhChoice(new QQ());
    }
}
View Code

4.虚拟机动态分配的实现

  动态分派的方法版本选择过程需要运行时在类的方法元数据区中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能考虑,大部分实现都不会真正的进行搜,而是通过在类的方法区建立一个虚方法表,使用虚方法表来代替元数据查找以提高性能。

   虚方法表中存放各个方法的入口。如果某个方法在子类中没有被重写,那子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址会替换为指向子类实现版本的入口地址。为了实现方便,具有相同签名的方法,在父类、子类的虚方法表中应当具有相同的索引号,这样当类型变换时便于查找。方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

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

  JAVA编译器输出的指令流基本上是一种基于栈的指令集架构,指令流中的大部分都是零地址指令,它们依赖操作数栈进行工作。基于栈的指令集主要优点是可移植,代码更加紧促、编译器实现更简单,缺点是执行速度相对来说会稍慢一些。

  基于栈的解释执行过程如下:

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

   程序字节码如下:

  执行过程如下:

posted @ 2016-07-27 18:41  简单爱_wxg  阅读(330)  评论(0编辑  收藏  举报