Java虚拟机字节码执行引擎

时间:2021-02-27 17:08:27

      1  运行时栈帧结构

         栈帧(Stack Frame) 是用于虚拟机执行时方法调用方法执行时的数据结构,它是虚拟栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。

       每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了,并且写入到方发表的 Code 属性之中,一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。

      一个线程中方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与之相关联的方法称为当前方法(Current Method)

        在概念模型上,典型的栈帧主要由 局部变量表(Local Stack Frame)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、返回地址(Return Address)组成

        局部变量表:是一组变量值的存储空间,用于存放 方法参数局部变量。在Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量。

       

          虚拟机通过索引定位的方式使用局部变量表。之前我们知道,局部变量表存放的是方法参数和局部变量。当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。

          为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。当离开了某些变量的作用域之后,这些变量对应的 Slot 就可以交给其他变量使用。这种机制有时候会影响垃圾回收行为。

          

        操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java 虚拟机的解释执行引擎称为”基于栈的执行引擎“,这里的栈就是指操作数栈。

         方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。

        在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。

          动态连接:

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

          Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

       

          当一个方法开始执行以后,只有两种方法可以退出当前方法:

  • 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
  • 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

        当方法返回时,可能进行3个操作:

  • 恢复上层方法的局部变量表和操作数栈
  • 把返回值压入调用者调用者栈帧的操作数栈
  • 调整 PC 计数器的值以指向方法调用指令后面的一条指令
          方法调用

         方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程。我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是符号引用,而不是方法在实际运行时的内存布局入口地址,也就是说符号引用解析成直接引用的过程。这个特性使得Java 具有强大的动态扩展能力,但也使得 Java方法调用过程变得复杂起来,需要在类加载器件,甚至是运行期间才确定目标方法的直接饮用。

         

在类加载的解析阶段,会将其中一部分符号引用直接转化为直接饮用,前提是:方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。

符合这个要求的方法,主要包括:静态方法私有方法。因为这两个方法的特点就决定了他们都不可能通过继承或别的方式重写其他版本。

与之相对应的,Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:

  • invokestatic:调用静态方法
  • invokespecial:调用<init>方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象
  • invokedynamic:会在运行时动态解析出调用电限定符所引用的方法,然后再执行该方法。
只能被 invokestatic 和 invokespecial 调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有 静态方法、私有方法、实例构造器、父类方法4类,这些方法称为 非虚方法,由于 final 修饰的方法不能被覆盖,也属于非虚方法。与之相反,其他的方法称为虚方法。
       分派调用

        依赖于静态类型来定位方法执行版本的分派动作(如重载)称为静态分派。

      

         静态类型和实际类型都可以发生变化,区别是静态类型仅仅在使用时变化,变量本身的静态类型不会改变,并且最终的静态类型是编译可知的。而实际类型变化结果只有在运行期才确定,编译器在编译程序的时候,不知道一个对象的实际类型是什么。
虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此,在编译期,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human),并把这个方法的符号引用写入 main() 方法的两条 invokevirtual 指令的参数中。
        运行时期,依赖于 实际类型来定位方法执行的分派动作(重写 Override) 属于动态分派。

        invokevirtual 指令的运行时解析过程大致分为以下几个步骤:
  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记做 C。
  2. 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。如果通过,则返回这个方法的直接饮用,查找过程结束:如果不通过,则返回 java.lang.IllegalAccessError 异常。
  3. 否则,按照继承关系从下往上依次对C 的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为 单分派多分派两种

         

在静态分派的过程中,选择目标方法的依据有两点:1、看对象的静态类型时什么,即使 Father 还是 Son。 2、方法参数的类型和数量是什么是 QQ还是 360 。因为是根据两个宗量进行选择,所以 Java 语言的 静态分派属于多分派类型。
在动态分派的过程中,由于编译器已经决定了目标方法的签名,因此只需要找到方法的接受者就可以了。因为是根据一个宗量进行选择,所以 Java 语言的 动态分派属单分派类型
        由于动态分配是非常频繁的动作,而且动态分配的方法版本选择过程需要运行时在 类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中,基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。最常用的手段就是为类在方法去中建立一个 虚方法表(Virtual Method Table , 也称为 vtable ,与此对应的,在 invokeinterface 执行时也会用到 接口方法表-Inteface Method Table , 简称 itable),使用虚方法表索引来代替元数据查找以提高性能。

      

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和弗雷相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口。如上图,Son 重写了来自于 Father 的全部方法,因此 Son 的方发表没有指向 Father 类型数据的箭头。但是 Son 和 Father 都没有重写来自于 Object 的方法,所以他们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。
方法表一般在类加载的 连接阶段进行初始化,准备了类的变量初始值之后,虚拟机会把该类的方法表也初始化完毕。
        http://blog.csdn.net/zq602316498/article/details/38981213