虚拟机字节码执行引擎

  • Post author:
  • Post category:其他




虚拟机字节码执行引擎



运行时栈帧结构

栈帧是虚拟机进行方法调用和执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧中存储了局部变量表、操作数栈、动态链接、方法返回地址等信息,每个方法的调用直到完成都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。



局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。在将Java文件编译为Class文件时,就在方法的Code属性max_locals数据项中确定了该方法所要分配的局部变量表的最大空间。局部变量表的容量以变量槽(slot)为最小单位。

局部变量表建立在线程的堆栈上,是线程私有的,不会出现线程安全问题。虚拟机通过索引定位方式使用局部变量表,索引范围从0到局部变量表的最大的slot数量。对于非static的实例方法,局部变量表中第0位的slot默认传递的是方法所属对象的引用(即this指针)。slot是可以复用的,若当前字节码的PC值已经超出某变量的作用域,那么该变量对应的slot就可以被其他变量复用,以此来节省栈空间。



操作数栈

操作数栈又称为操作栈,是一个后入先出栈,其最大深度在编译的时候写入到Code属性的max_stacks数据项中,在方法执行时操作栈的深度不能超过max_stacks属性设定的最大值。方法开始时为空,方法执行过程中会有各种字节码指令往操作数栈中写入或弹出,也就是对应着入栈和出栈操作。

多个操作数栈是互相独立的,但是大多数的虚拟机实现会做一些优化,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠起来,通过这些共享区域,使得方法调用时可以共用一部分数据,避免额外的参数复制传递。



动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态链接。一部分符号引用在类加载或第一次使用时转化为直接引用,叫做静态解析;另外一部分符号引用在每次运行期间转化为直接引用,叫做动态链接。



方法返回地址

方法执行后有两种方式退出该方法:

  1. 执行引擎遇到一个方法返回的字节码指令,将返回值传给调用者,该方式称为正常完成出口
  2. 方法执行过程中遇到了异常而在方法体中未处理,导致方法退出,这种方式称为异常完成出口,它不会产生任何的返回值给它的调用者

方法退出后要回到方法被调用的位置,程序才能继续执行,方法返回时要在栈帧中保存一些信息,用来帮助恢复到它的上层方法的执行状态。方法退出等同于把当前栈帧出栈:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC值以指向方法调用者指令的后一条指令。



方法调用

方法调用不等同于方法执行。方法调用是为了确定被调用方法的版本,暂不涉及方法内的具体运行。所有的方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内部内存中的入口地址(即直接引用),在类加载甚至是运行期间才能确定目标方法的直接引用。



解析

调用目标在程序写好、编译器编译时就确定下来的这类方法的调用称为解析。主要有静态方法和私有方法两大类,它们都是在类加载的阶段解析。

在JVM里提供了5条方法调用的字节码指令:


  1. invokestatic

    调用静态方法

  2. invokespecial

    调用实例构造器

    <init>

    、私有方法或父类方法

  3. invokevirtual

    调用虚方法

  4. invokeinterface

    调用接口方法,在运行时确定实现该接口方法的对象

  5. invokedynamic

    先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

前四条指令的分配逻辑是固定在Java虚拟机内部的,而第五条指令的分派逻辑是根据用户设定的引导方法决定的。解析调用是一个静态过程,在编译期就能完全确定下来,在类加载时就会将符号引用转化为直接引用,不会延迟到运行期才去执行。



分派



静态分派

所有依赖静态类型来定位方法执行版本的分派动作叫做静态分派。静态分派发生在编译期,不是由虚拟机执行的,它是重载的本质。



动态分派

在运行期确定接收者的实际类型以确定方法执行版本的分派动作称为动态分派。动态分派发生在方法实际运行时期,它是重写的本质。



单分派与多分派

方法的接收者和方法参数统称为宗量。根据分派基于多少种宗量,可以划分为单分派和多分派。Java语言的静态分派属于多分派;动态分派属于单分派。



JVM动态分派的实现

常用的稳定优化手段:为类在方法区建立一个虚方法表(接口方法表),用虚方法表索引来代替元数据查找以提高性能,虚方法表中存储了各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表中的方法入口地址和父类相同方法的入口地址是一样的,都指向父类的方法入口地址;如果子类重写了父类方法,那么子类虚方法表中的方法入口地址则指向子类实现的方法入口地址。

除了使用虚方法表这一稳定的优化手段外,如果条件允许,还会使用内联缓存和基于类型继承关系分析这两种非稳定的激进优化方法,来获得比较高的性能。



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

  1. 解释执行

    程序源码 –> 词法分析 –> 单词流 –> 语法分析 –> 抽象语法树 –> 指令流 –> 解释器 –>解释执行

  2. 编译执行

    程序源码 –> 词法分析 –> 单词流 –> 语法分析 –> 抽象语法树 –> 优化器 –> 中间代码 –> 生成器 –>目标代码

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

    Java编译器输出的指令流基本上是一种基于栈的指令集架构,指令大部分为零地址指令,依赖操作数栈工作。具有可移植性好、代码紧凑、编译器实现简单的优点,但执行速度较慢。

    基于寄存器的指令集依赖寄存器工作,寄存器由硬件直接提供,所以会受到硬件条件的约束,但是这种架构与基于栈的指令集架构相比,完成相同功能所需要的指令数量一般会比较少,速度会更快。

  4. 基于栈的解释执行

    中间变量都以操作数栈的出栈和入栈做为信息交换途径。



参考文献

深入理解Java虚拟机



版权声明:本文为foolishboy_w原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。