深入理解Java虚拟机-- java虚拟机字节码执行引擎浅析

来源:互联网 发布:炫浪网络社区下载不了 编辑:程序博客网 时间:2024/06/06 03:20

本文是深入理解java虚拟机的读书笔记


执行引擎是java虚拟机的核心组成部分之一。
    我们知道,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树、再遍历语法树生成线性的字节码指令流的过程。而字节码文件再经过加载、验证、准备、解析、初始化等阶段才能被使用。字节码执行引擎正是执行了这样的过程输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时栈帧结构:
栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈中存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
     每一个栈帧都包含了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译 程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
       注:对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只是针对当前栈帧进行操作。
局部变量表一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽为最小单位,每个slot都应该能存放一个boolean,byte,short,int,char,float,reference,returnAddress类型的数据,对于64位的数据类型只有double,long两种(reference可能为32位也可能为64位),这两种类型占用两个slot。
    虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法(非static)那么局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用,方法中可以通过this来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量slot,参数表分配完毕之后,再根据方法体内部定义的变量顺序和作用域分配其余的slot。
注:类变量有两次赋值的过程,一次在准备阶段,赋予系统初始值(比如int默认值为0,boolean默认值为false,object类型默认值为null等),另外一次在初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶段程序员没有为类变量赋值也没用关系,类变量仍然具有一个确定的初始值。但是局部变量若是定义了但没有赋初始值是没法使用的,类加载将会失败。

操作数栈即用来存放操作数的栈结构,当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈和出栈的操作。
注:java虚拟机的解释执行引擎称为基于栈的执行引擎,其中所指的栈就是操作数栈。

动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。我们知道class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化为称为静态解析,另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

方法返回地址:方法被执行后,有两种方式退出这个方法。第一种方法是执行引擎遇到任意一个方法的返回的字节码指令。另外一种退出方式是在方法执行过程中遇到了异常,并且这个异常并没有在方法体中得到处理。方法退出之后,需要返回到方法被调用的位置,程序才能继续执行,方法返回时需要在栈帧中保存一些信息,用以帮助它恢复它上层方法的执行状态。一般情况下,调用者的pc计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值,方法异常退出时,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。
    方法退出的过程实际上等同于把当前栈帧出栈,所以可能需要执行这些操作:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈的操作数栈中,调整pc计数器的值。
附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,这部分信息取决于具体的虚拟机实现。

方法调用
方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部的具体运行过程。class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址,这使得java有着更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂起来,需要在类的加载甚至运行期间才能确定目标方法的直接引用。

解析调用:之前说到,所有方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是,方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的
符合这个条件的有静态方法,私有方法,实例构造器和父类方法四类,它们在类加载的时候会把符号引用解析为该方法的直接引用。
    解析调用一定是一个静态的过程,编译期间就完全确定,在类装载的解析阶段就会把涉及到的符号引用
全部转化为可确定的直接引用,不会延迟到运行期间再去完成。
分派调用:分派调用可能是静态的也可能是动态的,根据分派依据的宗量数又可分为单分派和多分派。分派机制与java的多态机制关系密切。
          1. 静态分派依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的最典型的应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的
          2.动态分派:在运行期间根据实际类型来确定方法执行版本的分派调用过程称为动态分派。这跟多态性的另一个体现——重写有着很密切的关联。
          3.单分派:根据一个宗量对目标方法进行选择
          4.多分派:根据多于一个的总量对目标方法进行选择。
注:方法的接收者与方法的参数统称为方法的宗量。
jdk1.6时期的java语言是一种静态多分派、动态单分派的语言。

静态分派的演示:
[cpp] view plain copy
  1. package b;  
  2. public class BB  
  3. {  
  4.     static abstract class Human  
  5.     {       
  6.     }  
  7.     static class Man extends Human  
  8.     {     
  9.     }  
  10.     static class Woman extends Human  
  11.     {        
  12.     }  
  13.       
  14.     public void sayHello(Human guy)  
  15.     {  
  16.         System.out.println("hello guy");  
  17.     }  
  18.     public void sayHello(Man guy)  
  19.     {  
  20.         System.out.println("hello man");  
  21.     }  
  22.     public void sayHello(Woman guy)  
  23.     {  
  24.         System.out.println("hello woman");  
  25.     }  
  26.       
  27.     public static void main(String[] args)  
  28.     {  
  29.         BB b = new BB();  
  30.         Human man = new Man();//静态类型为Human  
  31.         Human woman = new Woman();  
  32.           
  33.         b.sayHello(man);  
  34.         b.sayHello(woman);  
  35.     }  
  36. }  
执行结果是:
hello guy
hello guy
原因:虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据,并且静态类型是编译期可知的,所以在编译阶段,javac编译器就根据参数的静态类型决定使用哪个重载版本,并把这个方法的符号引用写入invokevirtual指令的参数中。

动态分配的演示:
[java] view plain copy
  1. package b;  
  2. public class AA  
  3. {  
  4.     static abstract class Human  
  5.     {  
  6.         protected abstract void sayHello();  
  7.     }  
  8.       
  9.     static class Man extends Human  
  10.     {  
  11.         @Override  
  12.         protected void sayHello()  
  13.         {  
  14.             System.out.println("man say hello");  
  15.         }  
  16.     }  
  17.     static class Woman extends Human  
  18.     {  
  19.         @Override  
  20.         protected void sayHello()  
  21.         {  
  22.             System.out.println("woman say hello");  
  23.         }  
  24.     }  
  25.       
  26.     public static void main(String[] args)  
  27.     {  
  28.         Human man = new Man();  
  29.         Human woman = new Woman();  
  30.         man.sayHello();  
  31.         woman.sayHello();  
  32.         man = new Woman();  
  33.         man.sayHello();  
  34.     }  
  35. }  
执行结果:
man say hello
woman say hello
woman say hello
原因:
invokevirtual指令有多态查找的机制,该指令的运行时解析过程步骤如下:
1.找到操作数栈顶的第一个元素所指向的对象的实际类型,记做c
2.如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError.
3.否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
4.始终没找到合适的方法,抛出java.lang.AbstractMethodError异常。
这就是java语言中方法重写的本质。


方法的执行

解释执行

在jdk 1.0时代,Java虚拟机完全是解释执行的,随着技术的发展,现在主流的虚拟机中大都包含了即时编译器(JIT)。因此,虚拟机在执行代码过程中,到底是解释执行还是编译执行,只有它自己才能准确判断了,但是无论什么虚拟机,其原理基本符合现代经典的编译原理,如下图所示:

此处输入图片的描述
在Java中,javac编译器完成了词法分析、语法分析以及抽象语法树的过程,最终遍历语法树生成线性字节码指令流的过程,此过程发生在虚拟机外部。

基于栈的指令集与基于寄存器的指令集

Java编译器输入的指令流基本上是一种基于栈的指令集架构,指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。另外一种指令集架构则是基于寄存器的指令集架构,典型的应用是x86的二进制指令集,比如传统的PC以及Android的Davlik虚拟机。两者之间最直接的区别是,基于栈的指令集架构不需要硬件的支持,而基于寄存器的指令集架构则完全依赖硬件,这意味基于寄存器的指令集架构执行效率更高,单可移植性差,而基于栈的指令集架构的移植性更高,但执行效率相对较慢,初次之外,相同的操作,基于栈的指令集往往需要更多的指令,比如同样执行2+3这种逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例):

iconst_2  //常量2入栈istore_1  iconst_3  //常量3入栈istore_2iload_1iload_2iadd      //常量2、3出栈,执行相加istore_0  //结果5入栈

而基于寄存器的计算流程:

mov eax,2  //将eax寄存器的值设为1add eax,3  //使eax寄存器的值加3


基于栈的代码执行示例

下面我们用简单的案例来解释一下JVM代码执行的过程,代码实例如下:

public class MainTest {    public  static int add(){        int result=0;        int i=2;        int j=3;        int c=5;        return result =(i+j)*c;    }    public static void main(String[] args) {        MainTest.add();    }}


使用javap指令查看字节码:

  public MainTest();    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 2: 0  public static int add();    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=4, args_size=0     //栈深度2,局部变量4个,参数0个         0: iconst_0  //对应result=0,0入栈         1: istore_0  //取出栈顶元素0,将其存放在第0个局部变量solt中         2: iconst_2  //对应i=2,2入栈         3: istore_1  //取出栈顶元素2,将其存放在第1个局部变量solt中         4: iconst_3  //对应 j=3,3入栈         5: istore_2  //取出栈顶元素3,将其存放在第2个局部变量solt中         6: iconst_5  //对应c=5,5入栈         7: istore_3  //取出栈顶元素,将其存放在第3个局部变量solt中         8: iload_1   //将局部变量表的第一个slot中的数值2复制到栈顶         9: iload_2   //将局部变量表中的第二个slot中的数值3复制到栈顶        10: iadd      //两个栈顶元素2,3出栈,执行相加,将结果5重新入栈        11: iload_3   //将局部变量表中的第三个slot中的数字5复制到栈顶        12: imul      //两个栈顶元素出栈5,5出栈,执行相乘,然后入栈        13: dup       //复制栈顶元素25,并将复制值压入栈顶.        14: istore_0  //取出栈顶元素25,将其存放在第0个局部变量solt中        15: ireturn   //将栈顶元素25返回给它的调用者      LineNumberTable:        line 4: 0        line 5: 2        line 6: 4        line 7: 6        line 8: 8  public static void main(java.lang.String[]);    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=1, args_size=1         0: invokestatic  #2                  // Method add:()I         3: pop         4: return      LineNumberTable:        line 12: 0        line 13: 4}


执行过程中代码、操作数栈和局部变量表的变化情况如下:

指令0执行

指令1执行

指令2执行

指令3执行

指令4执行

指令5执行

指令6执行

指令7执行

指令8执行

指令9执行

指令10执行

指令11执行

指令12执行

指令13执行

指令14执行

指令15执行


  1. 也成为容量槽,虚拟规范中并没有规定一个Slot应该占据多大的内存空间。 ↩
  2. 这里的严格匹配指的是字节码操作的栈中的实际元素类型必须要字节码规定的元素类型一致。比如iadd指令规定操作两个整形数据,那么在操作栈中的实际元素的时候,栈中的两个元素也必须是整形。↩
  3. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。↩
  4. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。↩
  5. 宗量:方法的接受者与方法的参数称为方法的宗量。
    举个例子:
    public void dispatcher(){int result=this.execute(8,9);}public void execute(int pointX,pointY){//TODO}


    在dispatcher()方法中调用了execute(8,9),那此时的方法接受者为当前this指向的对象,8、9为方法的参数,this对象和参数就是我们所说的宗量。
http://blog.csdn.net/chdjj/article/details/23468761

http://blog.csdn.net/dd864140130/article/details/49515403

阅读全文
0 0
原创粉丝点击