在前面两篇文章,我们介绍了类加载子系统和运行时数据区
本篇我们就来看看 JVM 的最后一个部分,执行引擎。
JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM锁识别的字节码指令、符号表和其他辅助信息。
那么,如果想让一个Java程序运行起来,这就涉及到了JVM的字节码执行引擎。执行引擎其实就是个运算器,能识别输入的指令,并根据输入指令执行一套特定的逻辑,最终输出特定的结构(对应平台上的本地机器指令)。
相比于JVM,物理机的执行引擎是由硬件实现的(CPU),而虚拟机的执行引擎由于自己实现的,但它们的处理流程是相似的:
- 取值:JVM 内部定义了两百多个字节码指令,不同字节码指令的实现机制都是不同的
- 译码:JVM 取出字节码指令后,需要将其翻译为不同的逻辑,然后才能执行(译码的作用)
- 执行:分为两个阶段,取操作数与进行运算
- 不断重复上面过程,最终输出输出对应平台上的本地机器指令
注意:在 jdk 1.0时代,Java虚拟机完全是解释执行的,随着技术的发展,现在主流的虚拟机中大都包含了即时编译器(JIT),所以,当前Java字节码的执行有两种方式:
- 即时编译方式(执行快):解释器先将字节码编译成机器码,然后再执行该机器码。
- 解释执行方式(启动快):解释器通过每次解释并执行一小段代码来完成Java字节码程序的所有操作。
通常采用的是第二种方法。由于JVM规格描述具有足够的灵活性,这使得将字节码翻译为机器代码的工作具有较高的效率。对于那些对运行速度要求较高的应用程序,解释器可将Java字节码即时编译为机器码,从而很好地保证了Java代码的可移植性和高性能。但在虚拟机在真正执行代码过程中,到底是解释执行还是编译执行,只有它自己才能准确判断了。
基于栈的代码执行示例
在上一篇文章中,我们介绍了每个虚拟机栈都是由一个或多个方法的栈帧构成,而每个栈帧又是由局部变量表、操作数栈、动态链接、方法出口。现在我们通过一个例子,来看看执行引擎到底是怎样译码并执行的。
1)示例代码
public class Demo {
public int math() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
// 入口
public static void main(String[] args) {
Demo demo = new Demo();
int math = demo.math();
}
}
2)编译并且class文件,然后将其反编译成我们能看懂的文件。
这里再额外提一句,编译过程:Demo.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Demo.class文件
javac Demo.java // person.class都是不可读的字节码文件(注:若不可读则用sublime打开)
javap -c -l Demo > Demo.txt // 反编译当前class文件,生成汇编代码,并重定向到Person.txt
得到的汇编代码如下图所示,这里只对math方法的指令进行了注释,更多JVM指令可以查阅 指令助记符总结及基本指令大全。
3)我画了个每条指令执行的流程图,math 函数的执行其实就是靠操作数栈和局部变量表的协作完成。
最后再说一句,若栈调用过深,那么可能会抛出异常 StackOverFlowError(栈溢出)。