【温故知新-Java虚拟机篇】5.字节码执行引擎

来源:互联网 发布:东北师大网络教育2018 编辑:程序博客网 时间:2024/06/10 01:39

该系列博客暂且定义为《深入理Java解虚拟机》的笔记,有些坑等后续看完书再填,有不对的地方多指教。

上一节我们讨论了类的加载过程。这期我们讨论运行时的内存布局。


1.运行时栈帧结构

        栈帧(Stack Frame)是用于支持虚拟机进行方法调用方法执行的数据结构,他是虚拟机运行时数据区中虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
        代码编译的时候,栈帧需要多大的局部变量表(max_locals),多深的操作数栈(max_stacks)都已经在Code属性中确定了,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。
        一个线程的方法调用链可能会很长,每个方法一个栈帧,栈顶为当前栈帧,与这个栈帧关联的方法称为当前方法。

1)局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量
a.局部变量表的容量以变量槽(Variable Slot,简称Slot)为最小单位,虚拟机规范中没有规定一个Slot应占用多大内存,只是导向性的说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或者returnAddress。
b.对于reference(对象引用)数据,也没有规范它的长度或者具体内容,一般要求做到两点:
1.从引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引。
2.此引用中直接或间接地查找到对象所属数据类型在方法区中存储的类信息

c.对于64位类型数据,虚拟机会以高位对齐方式分配两个Slot,如long、double。

d.方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非 static),那么局部变量表中的第0位索引的Slot默认是this引用。其余参数按照声明顺序分配,然后再给方法体中的变量分配内存。

e.变量表中的Slot是可以重用的,当一个变量超出了作用域,那么这个变量所占的Slot既可以被其他变量使用。

2)操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出的(LIFO)栈。
a.操作数栈的每一个元素可以是任意的Java数据类型,32位数据类型所占的栈容量为1,64位为2。
b.当一个方法执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取数据,比如做算术运算,或者调用其他方法使用操作数栈传递参数。
c.概念模型中每个栈帧是相互独立的,但实现中,多数虚拟机都做一些优化,比如让上下两个栈帧的部分局部变量表重叠在一起,这样方法调用时,就不需要进行额外的复制传递。

3)动态链接

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

b.字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载或者第一次次使用的时候转化为直接引用,这种转换成为静态解析。另一部分将在每一次运行期间转化,这部分称为动态链接

4)方法返回地址

a.方法的退出有两种方式:1.执行引擎遇到返回指令。2.出现异常,并且没有捕获,或者自定义异常。
b.方法退出实际就是将当前帧出栈,然后恢复栈顶(调用方法)的局部变量表和操作数栈,然后将结果入栈调整PC计数器的值(PC计数器记录当前指令地址),指向方法调用后的一条指令等。

2.方法调用

方法调用不等同与方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),不会执行方法。

1)解析

a.对于“编译期可知,运行期不可变的这类方法调用称为解析(Resolution),主要包含静态方法和私有方法,前者与类直接关联,后者外部不可访问,运行期间是不可变的。
b.虚拟机里面提供了5条方法调用字节码指令,分别如下:
  • invokestatic:调用静态方法。
  • invokespecial:调用实例构造方法(<init>())、私有方法和父类方法。
  • invokevirtual:调用所有的虚方法。
  • invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象。
  • invokedynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,前面四条指令的分派逻辑是固化在Java虚拟机内部的。而invokedynamic是由用户所设定的引导方法决定的。
  • 前两个指令调用方法,都可以在解析阶段确定唯一调用版本,在加载的时候就会将符号引用解析为直接引用。

2)分派

a.静态分派


package com.adiaixin.java;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 human) {        System.out.println("Hello Man!");    }    public void sayHello(Woman human) {        System.out.println("Hello Woman!");    }    public static void main(String[] arr) {        Human man = new Man();        Human woman = new Woman();        StaticDispatch sd = new StaticDispatch();        sd.sayHello(man);//Hello Human!        sd.sayHello(woman);//Hello Human!    }}

以上结果有些经验就应该可以看出结果,但是为什么重载选择执行参数为Human的方法呢?

Human man = new Man();

a.对于这个语句,我们把其中的“Human”称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type)。

b.静态类型和实际类型在程序中都是可以发生一些变化,区别是静态类型的变化仅仅发生在使用时(如使用类型转换),变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的;而实际类型变化的结果是运行期才可确定的,编译期在编译程序的时候并不知道一个对象的实际类型是什么。

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

c.提供方法的对象确定前提下,使用哪个重载版本,完全取决于传入参数的数量和类型虚拟机重载时是通过参数的静态类型作为判定依据的,所以选择了Human作为参数的版本。

d.所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

b.动态分派

与静态分派相反,动态分派根据实际类型来决定方法的版本,这个特性是Java中“方法重写(override)”的本质。
package com.adiaixin.java;/** * 动态分派代码,方法重写 */public class DynamicDispatch {    static abstract class Human {        protected abstract void sayHello();        @Override        public boolean equals(Object obj){            return true;        }    }    static class Man extends Human{        @Override        protected void sayHello() {            System.out.println("Man say Hello!");        }    }    static class Woman extends Human{        @Override        protected void sayHello() {            System.out.println("Woman say Hello!");        }    }    public static void main(String[] arr) {        Human woman = new Woman();        Human man = new Man();        man.sayHello();        woman.sayHello();        man = new Woman();        man.sayHello();    }}

相信了解重写的童鞋应该可以猜到运行结果为:
Man say Hello!
Woman say Hello!
Woman say Hello!
我们通过javap -verbose DynamicDispatch来查看这个类的class字节码文件。如下:

Compiled from "DynamicDispatch.java"public class com.adiaixin.java.DynamicDispatch extends java.lang.Object  SourceFile: "DynamicDispatch.java"  InnerClass:    #9= #2 of #7; //Woman=class com/adiaixin/java/DynamicDispatch$Woman of class com/adiaixin/java/DynamicDispatch   #11= #4 of #7; //Man=class com/adiaixin/java/DynamicDispatch$Man of class com/adiaixin/java/DynamicDispatch   abstract #13= #12 of #7; //Human=class com/adiaixin/java/DynamicDispatch$Human of class com/adiaixin/java/DynamicDispatch  minor version: 0  major version: 49  Constant pool:const #1 = Method#8.#30;//  java/lang/Object."<init>":()Vconst #2 = class#31;//  com/adiaixin/java/DynamicDispatch$Womanconst #3 = Method#2.#30;//  com/adiaixin/java/DynamicDispatch$Woman."<init>":()Vconst #4 = class#32;//  com/adiaixin/java/DynamicDispatch$Manconst #5 = Method#4.#30;//  com/adiaixin/java/DynamicDispatch$Man."<init>":()Vconst #6 = Method#12.#33;//  com/adiaixin/java/DynamicDispatch$Human.sayHello:()Vconst #7 = class#34;//  com/adiaixin/java/DynamicDispatchconst #8 = class#35;//  java/lang/Objectconst #9 = AscizWoman;const #10 = AscizInnerClasses;const #11 = AscizMan;const #12 = class#36;//  com/adiaixin/java/DynamicDispatch$Humanconst #13 = AscizHuman;const #14 = Asciz<init>;const #15 = Asciz()V;const #16 = AscizCode;const #17 = AscizLineNumberTable;const #18 = AscizLocalVariableTable;const #19 = Ascizthis;const #20 = AscizLcom/adiaixin/java/DynamicDispatch;;const #21 = Ascizmain;const #22 = Asciz([Ljava/lang/String;)V;const #23 = Ascizarr;const #24 = Asciz[Ljava/lang/String;;const #25 = Ascizwoman;const #26 = AscizLcom/adiaixin/java/DynamicDispatch$Human;;const #27 = Ascizman;const #28 = AscizSourceFile;const #29 = AscizDynamicDispatch.java;const #30 = NameAndType#14:#15;//  "<init>":()Vconst #31 = Ascizcom/adiaixin/java/DynamicDispatch$Woman;const #32 = Ascizcom/adiaixin/java/DynamicDispatch$Man;const #33 = NameAndType#37:#15;//  sayHello:()Vconst #34 = Ascizcom/adiaixin/java/DynamicDispatch;const #35 = Ascizjava/lang/Object;const #36 = Ascizcom/adiaixin/java/DynamicDispatch$Human;const #37 = AscizsayHello;{public com.adiaixin.java.DynamicDispatch();  Code:   Stack=1, Locals=1, Args_size=1   0:aload_0   1:invokespecial#1; //Method java/lang/Object."<init>":()V   4:return  LineNumberTable:    line 6: 0  LocalVariableTable:    Start  Length  Slot  Name   Signature   0      5      0    this       Lcom/adiaixin/java/DynamicDispatch;public static void main(java.lang.String[]);  //main方法  Code: //代码属性   Stack=2, Locals=3, Args_size=1   0:new#2; //class com/adiaixin/java/DynamicDispatch$Woman     3:dup   4:invokespecial#3; //Method com/adiaixin/java/DynamicDispatch$Woman."<init>":()V   7:astore_1   8:new#4; //class com/adiaixin/java/DynamicDispatch$Man   11:dup   12:invokespecial#5; //Method com/adiaixin/java/DynamicDispatch$Man."<init>":()V   15:astore_2   16:aload_2     //将变量2(即对象变量man)入栈   17:invokevirtual#6; //Method com/adiaixin/java/DynamicDispatch$Human.sayHello:()V,调用Human.sayHello虚方法   20:aload_1//将变量1(即对象变量woman)入栈   21:invokevirtual#6; //Method com/adiaixin/java/DynamicDispatch$Human.sayHello:()V,调用Human.sayHello虚方法   24:new#2; //class com/adiaixin/java/DynamicDispatch$Woman   27:dup   28:invokespecial#3; //Method com/adiaixin/java/DynamicDispatch$Woman."<init>":()V   31:astore_2   32:aload_2   33:invokevirtual#6; //Method com/adiaixin/java/DynamicDispatch$Human.sayHello:()V   36:return  LineNumberTable:    line 28: 0   line 29: 8   line 30: 16   line 31: 20   line 32: 24   line 33: 32   line 34: 36  LocalVariableTable:    Start  Length  Slot  Name   Signature   0      37      0    arr       [Ljava/lang/String;   8      29      1    woman       Lcom/adiaixin/java/DynamicDispatch$Human;   16      21      2    man       Lcom/adiaixin/java/DynamicDispatch$Human;}




字节码的main方法中,第17和21行都是调用常量池Human.sayHello()的虚方法,但是它是怎么找到真正执行的方法呢?invokevirtual指令的运行时解析过程大致分为以下几个 步骤:

1.找到操作栈顶的第一个元素所指向的实际类型(即分别是Man和Woman),记做C。

2.如果在类型C中找到与常量中的描述符和简单名都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError异常。

3.如果没有匹配到方法,则按照继承关系,从下往上依次对C的各个父类进行第二步搜索

4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

5.第二步的方法查找过程猜测:根据C的全限定名查找字节码文件有没有加载到方法区,如果已经完成,则通过简单名和描述符在方法表中匹配方法,如果没有加载,则先加载字节码文件

3)重载&&重写终极考试题

上面我们讲了重载和重写的执行原理,我们至少需要记住两点,既可以解决重载重写的调用问题:

a.选择哪个重载方法,是参数的静态类型决定的。

b.选择哪个对象的重写方法,是这个对象的实际类型决定的。

下面我们看下下面这道变态的题:

package com.adiaixin.java;public class DispatchCompoud {    static abstract class Animal{        protected String name = "Animal";        protected String getName(){return this.name;}    }    static class Tiger extends Animal{        protected String name = "Tiger";        @Override        protected String getName(){return this.name;}    }    static class Rabbit extends Animal{        protected String name = "Rabbit";        @Override        protected String getName(){return this.name;}    }    static abstract class Human {        protected abstract void sayHello(Animal animal);        protected abstract void sayHello(Tiger tiger);    }    static class Man extends Human{        @Override        protected void sayHello(Animal animal) {            System.out.println("Man say Hello to Animal,name is " + animal.getName());        }        @Override        protected void sayHello(Tiger tiger) {            System.out.println("Man say Hello to Tiger,name is " + tiger.getName());        }    }    static class Woman extends Human{        @Override        protected void sayHello(Animal animal) {            System.out.println("Woman say Hello to Animal,name is " + animal.getName());        }        @Override        protected void sayHello(Tiger tiger) {            System.out.println("Woman say Hello to Tiger,name is " + tiger.getName());        }    }    public static void main(String[] arr) {        Human woman = new Woman();        Human man = new Man();        Animal rabbit = new Rabbit();        Animal tiger = new Tiger();        man.sayHello(tiger);        man.sayHello(new Tiger());        woman.sayHello(rabbit);        woman.sayHello(new Rabbit());    }}



Animal 有一个name属性,一个getName()方法,Tiger和Rabbit继承这个类,并重写了这个方法。

Human有两个抽象方法,都为sayHello,参数分别为Animal和Tiger。

我们先看第一个输出:man.sayHello(tiger):

a.tiger的静态类型为Animal,所以调用的一定是名称为sayHello,参数为Animal的方法。

b.又因为man的实际变量是Man,所以调用的是Man类中的sayHello(Animal animal)方法。

c.到方法里面,Animal的实际类型是Man,所以调用getName()是调用Man的getName()。

我们可以用上面的逻辑,来算下面的三个输出,其中new Tiger()的静态类型就是Tiger,你都算对了么,答案如下:

Man say Hello to Animal,name is TigerMan say Hello to Tiger,name is TigerWoman say Hello to Animal,name is RabbitWoman say Hello to Animal,name is Rabbit

3.解释器执行过程实例

书上的例子解释已经很透彻,我就不重新写,直接复制过来,让大家学习: