虚拟机字节码执行引擎
概述
“虚拟机”是一个相对于“物理机”的概念,这两种及其都有代码执行能力。
其区别主要在于:
- 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的。
- 虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集和执行引擎的体系结构,并且能够执行那些不被硬件直接支持的指令集格式。
所有Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
运行时栈帧结构
栈帧概述
栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。-
栈帧的内容
栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
栈帧的运行
对于执行的引擎中,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎的所有字节码指令都只对当前栈帧进行操作。
局部变量表
概述
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。-
局部变量表的数据结构
- 局部变量表以变量槽(Variable Slot)为最小单位,虚拟机中并未指明一个Slot的内存大小,但是有导向性的说明可以使用32位或者更小的物理内存。
- 如果是64位数据类型的变量,会同时使用两块连续的32位的Slot。对于这种情况,不允许单独访问其中的一个,如果有这种操作,则会在类加载的校验阶段抛出异常。
-
局部变量表的内容
- 第0位索引的Slot默认是用于传递方法所属对象实例的引用,可以通过
this
来访问到这个参数。 - 从1位索引开始分配形参
- 然后再分配局部变量
- 第0位索引的Slot默认是用于传递方法所属对象实例的引用,可以通过
-
局部变量表中Slot的重用
- 方法体中定义的变量其作用域并不会覆盖整个方法体(比如循环体中定义的变量),如果当前字节码PC计数器的值已经超出某个变量的作用域(比如退出循环),那么这个变量对应的Slot就可以交给其他变量使用。
- Slot的重用是为了节约栈帧空间,但是会伴随着一些副作用(比如影响到GC)
-
方法中的局部变量
- 方法中的局部变量并没有类变量在类加载时候的“准备阶段”,也就是没有赋初始值的阶段。
- 如果一个局部变量定义了但没有赋初值是不能使用的,但好在编译器能在编译阶段察觉到并给出错误。
操作数栈
- 概述
操作数栈(Operand Stack)也称为操作数栈,它是一个后入先出栈。 - 作用
当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码往操作数栈中写入和提取内容,也就是入栈和出栈操作。 -
栈帧的优化
在概念模型中,两个栈帧作为虚拟机栈的元素,是完全独立的。但在大多数虚拟机的实现里都会做一些优化,令两个栈帧出现一部分重叠。
动态连接
- 概述
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
- 概述
在方法退出(包括异常退出)之后,需要返回到方法被调用的位置,程序才能继续执行,方法返回是可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。 - 方法退出的方式
- 正常的方式(正常完成出口)
当执行引擎遇到任意一个方法返回的字节码指令时。这时候可能有返回值传递给上层的调用者。 - 异常的方式(异常完成出口)
当方法执行的过程中遇到异常。没有任何返回值返回给上层调用者。
- 正常的方式(正常完成出口)
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧里面,这部番完全取决于具体的虚拟机实现。
方法调用
解析
概括
Java中所有方法的目标方法在Class
文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这类方法的调用称为解析。-
方法调用的指令
在Java语言中符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法*两大类,前者与类型直接关联,后者在外部不可访问。
与之相对应的是Java在虚拟机里提供了五条方法调用指令:-
invokestatic
:调用静态方法 -
invokespecial
:调用实例构造器<init>方法、私有方法和父类方法。 -
invokevirtual
:调用所有虚方法 -
invokeinterface
:调用接口方法 -
invokedynamic
:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
-
非虚方法
只要能被invokestatic
和invokespecial
指令调用的方法和fina方法(final方法被invokevirtual
调用),都可以在解析阶段中确定唯一的调用版本,它们在类加载的时候就会把符号引用解析为该方法的直接引用。
分派
- 静态分派
所有通过静态类型来定位方法执行版本的分配动作称为静态分派。静态分派的经典应用是重载。 - 动态分派
在运行期根据实际类型确定方法执行版本的分配称为动态分配。动态分派的经典应用是重写。 - 单分派与多分派
- 概述
方法的接受者(调用方法的对象)与方法的参数统称为方法的宗量。根据分派基于多少宗量可以将分派划分为单分派和多分派。 - Java语言静态分派属于多分派
- Java语言动态分派属于单分派
- 概述
- 虚拟机动态分派的实现
- 背景
由于动态分配是非常频繁的操作,因此虚拟机在实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的操作。 - “稳定优化”手段
为类在方法区建立一个虚方法表,与此对应,在invokevirtual
执行的时候也会用到接口方法表。使用虚方法表索引来代替元数据查找以提高性能。
虚方法表一般在类加载的连接阶段进行初始化,装备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。 - “激进优化”手段
使用内联缓存和基于“类型继承关系分析”技术的守护内联两种非稳定的“激进优化”的手段来获得更高的性能。
- 背景
动态类型语言支持
基于栈的字节码解释执行引擎
-
解释执行
下面的那条分支是传统编译原理中程序代码到目标机器代码的生成过程
中间那条就是解释执行的过程
Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,这一部分是在Java虚拟机之外的过程。而解释器在虚拟机的内部,所以Java的编译就是半独立的实现。
-
基于栈的指令集与基于寄存器的指令集
基于栈的指令集的优点:- 可移植
- 代码相对紧凑
- 编译器实现更加简单
基于栈的指令集的缺点:
- 速度相对来说要慢些
总结
- 主要介绍了虚拟机进行方法调用和方法执行时的数据结构(栈帧),主要包括局部变量表、操作数栈、动态连接、方法返回地址和附加信息。
- 紧接着介绍方法的调用时虚拟机内部的实现方法,主要有解析和分派,分别适应于不同的类型的方法。
- 之后介绍了关于Java动态语言的支持,包括虚拟机指令
invokedynamic
和MethodHandle
. - 最后介绍了基于栈的字节码执行引擎,并分析了相对于基于寄存器的字节码寄存器的优缺点,然后再演示了基于栈的字节码解释器的执行过程。