《深入理解Java虚拟机》学习笔记(六)(虚拟机字节码执行引擎)

虚拟机字节码执行引擎

执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。

运行时栈帧结构

栈帧存储了方法的局部变量表、 操作数栈、 动态连接和方法返回地址等信息。 (详情见《深入理解Java虚拟机》(学习笔记(一)))

在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current StackFrame),与这个栈帧相关联的方法称为当前方法(Current Method)。

《深入理解Java虚拟机》学习笔记(六)(虚拟机字节码执行引擎)_第1张图片
图1 栈帧的概念结构

局部变量表

是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

《深入理解Java虚拟机》学习笔记(六)(虚拟机字节码执行引擎)_第2张图片
图1-1 局部变量表

局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位

《深入理解Java虚拟机》学习笔记(六)(虚拟机字节码执行引擎)_第3张图片
图1-2 各种数据类型所占据的Slot大小

如果执行的是实例方法(非static的方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。 其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

局部变量不存在“准备阶段”(详情见《深入理解Java虚拟机》(学习笔记(五)))。如果一个局部变量定义了但没有赋初始值是不能使用的。

操作数栈

是一个后入先出(Last In FirstOut,LIFO)栈

动态连接

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

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。

  • 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
  • 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。 一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中

方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本

一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址

解析

调用目标在程序代码写好、 编译器进行编译时就必须确定下来。 这类方法的调用称为解析(Resolution),解析的是非虚方法。
Java中5种方法调用字节码指令

  • invokestatic:调用静态方法。
  • invokespecial:调用实例构造器<init>方法、 私有方法和父类方法。
  • invokevirtual:调用所有的虚方法。
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
  • 非虚方法
    • 只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、 私有方法、 实例构造器、 父类方法4类(还有final方法,但是是用invokevirtual调用),它们在类加载的时候就会把符号引用解析为该方法的直接引用。

分派

  • 1.静态分派(属于多分派
    • 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
    • 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。 (Human human = new Man(),Human是静态类型,Man是实例类型)
    • 选择重载版本的过程是通过静态分派完成的。
  • 2.动态分派(属于单分派)
    • 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
    • 多次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。
  • 3.单分派与多分派
    • 宗量:方法的接收者与方法的参数统称为方法的宗量
      • 单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
    • Java语言是一门静态多分派、 动态单分派的语言。
  • 4.虚拟机动态分派的实现
    类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。 如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。 如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

具有相同签名的方法,在父类、 子类的虚方法表中都应当具有一样的索引序号

方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕

在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的“激进优化”手段来获得更高的性能

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

Java语言中,Javac编译器完成了程序代码经过词法分析、 语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。 因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。

你可能感兴趣的:(《深入理解Java虚拟机》学习笔记(六)(虚拟机字节码执行引擎))