虚拟机字节码执行引擎

143 阅读13分钟

概述

  1. 执行引擎是java虚拟机最核心的组成部分之一,“虚拟机”是一个相对的“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集。
  2. 在不同的虚拟机实现里面,执行引擎在执行java代码的时候,可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备。
  3. 从外观上来看,所有java虚拟机的执行引擎是一致的,输入字节码文件,处理过程是自驾吗解析的等效过程,输出的是执行结果。

运行时栈帧结构

  1. 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机的虚拟机栈栈元素。
  2. 栈帧存储了局部变量表、操作数栈、动态链接和方法返回地址等信息。
  3. 每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  4. 只有位于栈顶的栈帧,才是最有效的,称为当前栈帧,与这个栈帧相关的方法称为当前方法。

一、局部变量表

  1. 局部变量表是一组变量存储空间,用于存放方法参数和方法内定义的局部变量。
  2. 局部变量表以变量槽(slot)为最小单位。
  3. 由于局部变量建立在线程的堆栈上,是线程私有数据,无论读写两个连续的slot是否为原子操作,都不会引起线程安全问题。
  4. 在方法执行时,虚拟机使用局部变量表完成参数值到参数变量表传递过程,如果执行的是实例方法,那局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数,其余参数则按照参数表顺序排泄,占用从1开始的局部变量slot,参数表分配完毕后,在根据方法内部定义的变量,顺序和作用域分配其余slot。
  5. 为了尽可能节省栈空间,局部变量表slot是可以重用的,如果当前字节码的PC的计数器的值已经超过某个变量的作用域,那这个变量对应的slot就可以交给其他变量使用。
  6. 关于局部变量表,还有一点会对实际开发产生影响,就是局部变量不想前面介绍的类变量那样存在准备阶段,我们知道类变量有两次被赋初始值的过程,一次值准备阶段赋予系统初始值,另一次是在初始化阶段,赋予程序定义的初始化值每一次即使在初始化阶段,程序员没有Wie类变量赋值也没有关系,但是局部变量不一样,如果一个变动定义了,但是没有赋初始值,则不能使用。

二、操组数栈

  1. 操作数栈也称为操作栈,是一个后入先出栈。
  2. 操作数栈的每一个元素可以是任意的java的数据类型,包括long、double、等,64位的话,栈容量是2.
  3. 当一个方法刚刚开始执行的时候,这个方法的操作数栈为空
  4. 两个栈帧作为虚拟机的元素,是完全独立的,但在大多数虚拟机的实现里面会做一些优化处理,令两个栈帧有一些重叠。

三、动态链接

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

四、方法返回地址

  1. 方法开始有两种方式可以退出这个方法。

    i.第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候也可能会有返回值传递给上层的方法调用者,这种退出方法方式,称为正常完成出口。 ii.另一种退出方式是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是java虚拟机内部产生的异常,还是代码证使用athrow字节码指令产生的异常,只要在方法的异常表中没有搜到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,这种方法是不会给上层调用者产生任何返回值的。

  2. 方法退出的过程实际上等同于把当前栈帧出栈,一次退出时可能执行的操作有:

    i.恢复上层 方法的局部变量表和操作数栈。 ii.把返回值压乳调用者栈帧的操作数栈中。 iii.调整PC计数器的值,以指向方法调用指令后面的一条指令。

方法调用

  1. 方法调用并不等同于方法执行,方法调用阶段唯一的任务就是缺德调用方法的版本(即调用哪一个方法) 一、解析

  2. 所有方法调用中的目标方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的版本,并且这个方法调用版本在运行期是不可变的,换句话说,调用目标在程序代码写好,编译器进行编译时,就必须确定下来,这个方法调用称为解析。

  3. 在java中符号,在编译期可知,在运行期不可变,这个要求的方法主要包括静态方和私有方法两类。

  4. java虚拟机提供了5条方法调用字节码指令。

    i.invokeStatic:调用静态方法。

    ii.invokeSpecial:调用实例构造器的方法,私有方法和父类方法。

    iii.invokeVirtual:调用所有的虚方法。

    iiii.invokeInterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

    v.invokeDynamic:先在运行时动态解析调用点,限定符所引用的方法,然后在执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokeDynamic指令的分派逻辑是由用户所设定的引导方法决定的。

  5. 只要能被inbvokeStatic和invokeSpecial指令调用的方法都可以在解析阶段确定唯一的调用版本,如何这个条件的有静态方法、私有方法、实例构造器、父类方法4类,他们在类加载的时候就会吧符号引用解析为该方法的直接引用,这些方法称为非虚方法。

  6. 被final休市的方法,虽然final方法是使用invokeVirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也我也无需对方法接收者进行多态选择,所以final方法是一种非虚方法。

  7. 解析调用一定是静态的过程在编译期间就完全确定,在类装载的解析阶段就会把设计的符号引用变为可确定的直接引用,不会延迟到运行期在去完成,而分派调用则可能是静态的也可能是动态的,根据分派依据的宗数可以分为单分派和多分派,这两类分派方式的两两组合构成了静态单分派、静态多分派、动态单分派、动态多分派。 二、分派

  8. 静态分派

    例如:Human man = new Man()

    i.Human 陈伟变量的静态类型或者叫外观类型。

    ii.Man则称为变量的实际类型。

    iii.静态类型是在编译期可知的,而实际类型变化的结果是在运行期才确定。

    iiii.编译器是在重载时通过参数的静态类型而不是实际类型作为判断依据的。

    v.依赖静态类型来定位方法执行版本的分派作为静态分派。

    vi.静态分派的典型应用是方法重载。

    vii.编译器虽然能确定出了方法重载版本,但是在很多情况下,这个重载的版本不是“唯一的”,往往只能确定一个“更加”合适的版本。

    viii.静态方法在类加载期就进行解析,而静态方法也可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

  9. 动态分派

    i.动态分派与重写有着密切的联系。

    ii.invokeVirtual指令的运行时解析过程大致分为以下N个步骤:

     1).找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
     2).如果在类型C中找到与常量中的描述符合名称都相符的方法,则进行权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.llegalAccessError异常。
     3).否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
     4).如果始终没找到合适的方法,则抛出java.lang.abstractmethod异常。
    

    iii. 两次调用中的invokeVirtual指令吧常量中的类方法符号引用解析到了不同的直接引用上,这个就是java语言中方法重写的本质。

    iiii. 把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

  10. 单分派和多分派

    i.方法的接收者与方法的参数统称为方法的宗量。

    ii.根据分派基于多种宗量,可以分划为单分派和多分派,单分派是根虎一个宗量对目标方法进行选择,多分派则是个根据多于一个宗量对目标方法进行选择。

    iii.静态分派属于多分派类型,选择目标方法依据两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360,这次选择产生了两条invokevirtual指令。

    iiii.java中动态分派属于单分派。

  11. 虚拟机动态分派的实现

    i.由于动态分派是非常频繁的动作,而且动态分派的方法片选择过程需要在运行时再类的方法元数据中搜索合适的目标方法,此在虚拟机的实际实现中基于性能考虑,大部分实现都不会真正的如此频繁的搜索。

    ii.最常用的稳定优化手段就是在类方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。

    iii.虚方法表中存着各个方法的实际入口。

    iiii.具有相同签名的方法在父类、子类的虚方法中应当具有一样的索引号。

    v.方法表一般在类加载的链接阶段进行初始化,准备了类的变量的初始值后,虚拟机回吧该类的方法表也初始化完毕。

动态类型语言支持

一、动态类型语言

  1. 动态类型语言的关键特征是他的类型检查的主题过程是在运行期,而不是编译器。
  2. 在编译期就进行类型检查过程的语言就是常用的静态类型语言。
  3. 变量无类型而变量值才有类型,这个特点也是动态类型语言的一个重要特征。

二、java.lang.invoke包

  1. 这个包的主要目的是在之前单纯靠符号引用来确定调用的目标方法,这种方式以外提供了一种新的动态确定目标方法的机智,称为methodhandle。

  2. 仅站在java语言的角度来看,methodHandle的使用方法阿和效果与refection有众多相似之处,不过存在以下区别

    i.从本质上讲,reflection和methodHandle机制都是在模拟方法调用,但是reflection是在模拟java代码层次的方法调用,而mthodhandle是在模拟字节码层次的方法调用。

    ii.reflection中的java.lang.reflect.method对象远比methodhandle机制中的java.lang.invoke.methodhandle 对象所包含的信息多,前面是方法在java一端的全面映射,包含了方法的签名,描述符,以及方法属性表中各种属性的java端的表示方式,还包含了执行权限等运行期信息,而后者紧急包含了与执行该方法相关的信息,reflection是重量级儿methodhandle是轻量级。

    iii.由于methodhandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面做得各种优化,在methodHandle上也可以采用类似思路去支持,而通过反射调用方法不行。

三、invokeDynamic 指令

  1. invokeDynamic指令与methodhandle机制的作用是一样的。
  2. 它们可以想象成为了达成同一个目的,一个采用上层java代码和API来实现,另一个用字节码和class中其他属性和常量来完成。
  3. 每一处含有invokeDynamic指令的位置称为动态调用点,这条子类的第一个参数不再是代表方法符号引用的constant_method_info常量,而是一个constant_invokeDynamic_info 常量,从这个常量中可以得到了3项信息:引导方法、方法类型和名称。引导方法是有固定的参数,并且返回java.lang.invoke.callsite对象,这个代表要真正执行的方法引用,根据constant_invokeDynamic_info常量提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个callsite对象,最终调用执行的目标方法。

四、掌控方法分派规则

  1. invokeDynamic指令与前4条invoke指令最大的区别就是他的分派逻辑不由虚拟机决定而是由程序员决定。

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

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

  1. java编译器是基于栈的指令集架构,部分是零地址指令,它们依赖操作数栈进行工作,与之相对应的是基于寄存器的指令集。

  2. 基于站的指令集的有点是可移植性,但是执行速度相对来说会稍慢些。

我是菜鸟,希望大家多多留言讨论~谢谢!

我的笔记是看完深入理解java虚拟机的体会,是本好书,推荐给大家.