[10] 字节码执行引擎
[10] 字节码执行引擎

1. 执行引擎

执行引擎是 JVM 的核心组成部分之一,《Java虚拟机规范》中制定了执行引擎的概念模型,这个概念模型成为各大 JVM 发行商的执行引擎的统一外观(Facade)。
在不同的 JVM 实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎,所以说 Java 是半编译、半解释型编程语言。
notion image
物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而 JVM 的执行引擎则是由软件自行实现,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行不被硬件「直接支持」的指令格式。
在 HotSpot VM 的实现中,执行引擎包括了:解释器即时编译器垃圾回收器
notion image
大部分的 Java 程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过图中的各个步骤:
notion image

  • 橙色部分就是现代经典编译原理过程,如今基于物理机、Java 虚拟机,或者非 Java 的其他高级语言虚拟机(HLLVM)的语言,大多都会遵循这种思路,在执行前先对程序源码进行词法分析语法分析处理,再把源码转化为抽象语法树(Abstract Syntax Tree,AST)
  • 绿色分支就是解释执行的过程。
  • 蓝色分支就是传统编译原理中程序代码到目标机器代码的生成过程。
对于一门具体语言的实现来说,词法分析、语法分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是 C/C++ 语言。
也可以选择把其中一部分步骤(如生成抽象语法树之前的步骤)实现为一个半独立的编译器,这类代表是 Java 语言。
又或者把这些步骤和执行引擎全部集中封装在一个封闭的黑匣子之中,如大多数的 JavaScript 执行器。

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

JVM 以方法作为最基本的执行单元,栈帧则是用于支持 JVM 运行方法调用和方法执行背后的数据结构,它也是 JVM 运行时数据区中的虚拟机栈里的栈元素。
JVM 运行时栈帧结构
字节码的指令集架构(Instruction Set Architecture,ISA)都是基于栈来设计的,字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作,所以 JVM 的解释执行引擎被称为「基于栈的执行引擎」,解释器也被称为「基于栈的解释器」,里面的「栈」就是操作数栈。
由于 JVM 跨平台的设计,栈是所有计算机体系结构的基础,且不会因为平台的不同而产生较大差异,但不同的平台 CPU 架构指令集不同,所以不能基于寄存器来设计指令。
iconst_1
iconst_1
iadd
istore_0
使用字节码指令集来计算 1+1
mov  eax, 1
add  eax, 1
使用 x86 的二地址指令集来计算 1+1
根据栈来设计指令的优点:
  • 跨平台
  • 指令集小
  • 编译器容易实现
缺点:
  • 性能下降
  • 实现同样的功能需要更多的指令

3. 解释器

当 JVM 启动时,解释器(Interpreter)会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行。解析器真正意义上所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应的平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着根据 JVM 中程序计数器里记录的下一条需要被执行的字节码,继续执行解释执行。
在 Java 的发展历史中,一共有两套解释器:古老的字节码解释器模板解释器。字节码解释器在执行时通过纯软件代码模拟字节代码的执行,效率非常低下。
现在普遍使用的是模板解释器,模板解释器对于每个字节码的解释执行是用汇编语言编写的,效率更高,将每一条字节码和一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码,以提高解释器的性能。
无论是模板解释器还是字节码解释器,其过程仍然是不断取值,解释执行。
在 HotSpot VM 中:
  • Interpreter 模块:实现了解释器的核心功能。
  • Code 模块:用于管理 HotSpot VM 在运行时生成的本地机器指令。

3.1 工作过程

  1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于 JVM 的程序计数器
  1. 每当执行一项指令操作时(譬如加法运算),JVM 会将操作数栈中的两个操作数出栈相加,并把相加结果重新入栈,程序计数器接着更新下一条需要被执行的指令地址。
  1. 在方法执行的过程中,执行引擎还可能会通过存储在局部变量表中的对象引用准确定位到存储在 Java 堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息
notion image

3.2 解释过程

假设有这么一段 Java 代码被编译为 Class 文件:
public int fun(){
    int a = 100;
    int b = 200;
    int c = 300return(a + b)* c;
}
使用 javap -v -p 命令反编译查看 Class 文件对应的字节码指令:
public int fun();
Code:
	stack=2,locals=4,args_size=1
		0:bipush 100
		2:istore_1
		3:sipush 200
		6:istore_2
		7:sipush 300
		10:istore_3
		11:iload_1
		12:iload_2
		13:iadd
		14:iload_3
		15:imul
		16:ireturn
  • Stack=2:表示运行该代码需要深度为 2 的操作数栈。
  • Locals=4:表示运行该代码需要局部变量表的 4 个 Slot 数量。
  • Args_size=1:表示该方法的形参个数,第一个形参是 this 引用
notion image
notion image
notion image
notion image
notion image
notion image
notion image
上面的执行过程仅仅是一种概念模型,JVM 最终会对执行过程做一些优化来提高性能,实际的运作过程不一定完全符合概念模型的描述。
更准确地说,实际情况会和上面描述的概念模型差距非常大,这种差距产生的原因是 JVM 中解析器即时编译器(JIT 编译器)都会对输入的字节码进行优化,例如,在 HotSpot VM 中,有很多以 fast_ 开头的非标准字节码指令用于合并、替换输入的字节码以提升解释执行性能,而即时编译器的优化手段更加花样繁多。