基于栈的字节码解释执行引擎
许多 Java 虚拟机的执行引擎在执行 Java 代码的时候都有解释执行和编译执行两种选择。
解释执行
Java 语言经常被人们定位为“解释执行”的语言,JDK 1.0 时代,这种定义还是比较准确的,但当主流的虚拟机中都包含了即时编译器后,Class 文件中的代码到第会被解释执行还是编译执行,就成了虚拟机自己才能准确判断的事情。
如今,基于物理机、Java 虚拟机,或者非 Java 的其它高级语言虚拟机的语言,大都遵循基于现代经典编译原理的思路,在执行前对源码进行词法分析和语义分析处理,把源码转化为抽象语法树。对一门具体的语言实现而言,词法分析、语法分析以致后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是 C/C++ 语言。也可以选择把一部分步骤实现为一个半独立的编译器,这类代表是 Java 语言。又或者把执行引擎全部集中封装在一个封闭的黑匣子中,如大多数的 Javascript 执行器。
Java 语言中,Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法书生成线性的字节码指令流的过程。因为这一部分动作是在 Java 虚拟机之外进行的,而解释器在虚拟机的内部,所以 Java 程序的编译就是半独立的实现。
基于栈的指令集与基于寄存器的指令集
Java 编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作,与之相对的另外一套常用的指令集架构是基于寄存器的指令集,最典型的就是 x86 的二进制指令集。
举个简单的例子,分别使用这两种指令集计算 1+1,基于栈的指令集会是这样子的:
iconst_1 // 把 1 压入栈
iconst_1 // 把另一个 1 压入栈
iadd // 栈顶的两个值出栈,相加,把结果放回栈顶
istore_0 // 把栈顶的值放到局部变量表的第 0 个 Slot 中
如果基于寄存器,那程序可能回事这样的:
mov eax, 1 // 把 EAX 寄存器的值设为 1
add eax, 1 // 把 EAX 寄存器的值加 1
基于栈的指令集的主要优点是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地受到硬件的约束;主要缺点是执行速度相对会稍慢一些。
基于栈的解释器执行过程
接下来看下面这段代码在虚拟机中是如何执行的:
public int cal() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
使用 javap 分析如下:
public int cal();
descriptor: ()I
flags: ACC_PUBLIC
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
LineNumberTable:
line 6: 0
line 7: 3
line 8: 7
line 9: 11
执行过程如下:
需要注意的是,上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做一些优化来提高性能,实际情况和上面描述的差距非常大。