1. 概述
- 执行引擎是Java虚拟机核心的组成部分之一
- 虚拟机是相对于物理机而言的,这两种机器都有执行引擎,物理机执行引擎是基于cpu和操作系统来说的,指令集与硬件有深度绑定,而虚拟机执行引擎是由软件自己实现的,因为可以使用硬件不支持的指令集.
- JVM的作用就是将字节码加载到内存中,解释编译为机器指令使操作系统能够识别执行.而执行引擎在其中起到的作用就是将字节码指令转换为机器指令.
- 执行引擎的工作过程:
- 执行引擎从pc寄存器中获取要执行的指令索引
- 使用指令索引从操作数栈中获取要执行的指令
- 将指令转换为具体的机器指令
- 交给操作系统执行
- 执行引擎输入的是二进制流,处理过程就是将字节码转换为机器指令的过程,输出的就是执行结果.
2.java代码编译执行的过程
解释器:
jvm启动时根据语法规范对字节码逐行解释执行,将每行字节码文件的内容翻译成本地平台的机器指令执行
JIT编译器:
jvm直接将代码翻译成本地机器指令
jdk1.0时代,jvm只能通过解释器对字节码逐行解释执行,之后增加了可以直接生成本地代码的编译器,所以java是一种半解释半编译的语言
3. 机器码,指令,汇编语言概述
3.1 机器码
机器码又叫机器指令,是计算机能够识别运行的代码,由二进制编写.
机器码由于cpu能够直接识别执行,所以执行速度最快.
但是二进制与人类语言差别较大,直接编写二进制程序难度较高,容易出错.
3.2 指令
为了解决二进制机器码难以阅读,编写的情况,就出现了指令.
指令是指将特定的0/1二进制码简化成一个简单的英文单词(一般情况都是英文单词),如mov,inc等,便于阅读,相比于机器码来说,更容易被人类理解.
指令与硬件平台关联比较深,同一个指令,不同的硬件平台所对应的机器码可能就是不一样的.
总结: 指令就是特定机器码的抽象.
3.3 指令集
不同的平台对应的指令就称为指令集
如 X86平台对应的就是X86指令集
ARM平台对应的就是ARM指令集
3.4 汇编语言
指令虽然相比于二进制机器码简单易读了许多,但是可读性还是比较差,所以又发明了汇编语言
汇编语言中用助记符来代替机器指令的操作码,用地址符号(Symbol)或标记(Label)代替指令或操作数的地址
汇编语言还是要解释为指令才能在各个平台执行.
3.5 高级语言
在汇编语言的基础上,又开发出了高级语言,更接近人的语言,如java python等语言
高级语言想要在平台执行也是要转为指令才能在cpu执行的
3.6 字节码
字节码是一种特殊状态的文件,与硬件条件无关(跨平台)
字节码需要执行引擎来解释编译为指令,再由cpu执行
字节码的标准应用: java byteCode
3.7 总结
4. 解释器
解释器将字节码逐行解释为本地机器指令执行.
4.1 解释器的分类
解释器分为字节码解释器(古老)和模板解释器(现在普遍使用)
字节码解释器: 采用纯软件代码的形式模拟字节码的运行
模板解释器: 将每条字节码与一个模板函数关联,由模板函数直接生成机器码,极大提高解释器的性能
在HotspotVM中,解释器主要由 Interpreter 和 Code模块构成
Interpreter 实现了解释器的核心功能
Code 管理生成的机器码指令
4.2 解释器的作用和弊端
解释器将字节码转换为机器指令的方式解释运行源代码
弊端: 无论是字节码解释器还是模板解释器,效率都比较低下.
5. JIT编译器
JIT(Just in Time)编译器被称之为即时编译器
将执行过的代码缓存至方法区,再次执行时就不用再次编译,而是直接转为机器码执行,速度极高
解释器和JIT编译器同时运行的原因:
JIT编译器的效率极高,为什么还要使用解释器呢?
主要是因为解释器与JIT编译器相比:
- 解释器在jvm启动的时候就可以对字节码进行编译运行,而不用等待代码全部编译完成后再执行,响应速度快,
- 而JIT编译器就必须要代码编译完成后将本地代码解释为指令后才能够执行,响应速度较慢
- 相对来说,JIT编译器在随着代码的执行,利用热点代码探测技术,速度会越来越快,所以更适合对响应时间要求不高的场景,如服务端应用.
- JRocket就是只有JIT编译器的jvm
- 当JIT编译器激进优化不成立的时候,解释器可以充当备用机,增加了可靠性
如何证明JIT的存在:
热机状态: 服务器启动运行了一段时间之后
冷机状态: 服务器刚启动的时候
将热机状态的流量切给冷机状态下的服务器时,会导致冷机状态的服务器直接挂掉,这个例子就说明JIT的存在
5.1 热点代码探测
JIT编译器需要根据字节码执行的频率来确定是否要将字节码编译成本地机器指令,执行频率较高的代码就称为热点代码,JIT编译器就针对这些执行频率较高的代码进行深度优化,编译成本地机器指令,在下次执行时就不用再使用解释器解释了,而是直接执行本地机器指令,提升JAVA性能
5.1.1 热点代码
一个多次执行的代码就可以被称为热点代码,包括循环体中的循环代码,这些代码都发生方法执行过程中,因此也被称为栈上替换(OSC ON Stack ReplaceMent)
5.1.2 热点探测
热点探测功能:
HotSpotVM采用的热点探测方式主要是基于计数器的探测,共有两种计数器
- 方法调用计数器 Invocation Counter: 统计方法调用的次数
- 回边计数器 Back Edge Counter: 统计循环体执行的次数
探测次数:
探测次数Client和Server模式有不同的阈值
Client默认1500次 Server默认10000次
超过阈值就会触发JIT编译
阈值可以通过 -XX:compileThreshold来设定
方法计数器的过程:
- 判断是否编译过,如果是直接调用编译过的机器指令
- 如果没有,方法计数器+1
- 判断+1后是否超过阈值,如果没有执行解释器,逐行解释执行
- 如果超i过阈值,提交编译请求到JIT编译器,编译器编译后进行代码缓存(方法区)
回边计数器与方法计数器类似
唯一的区别在于,判断是否超过阈值时,是判断两个计数器之和.
可以理解,因为回边计数器中循环体内可能调用该方法
5.1.3 热度衰减
默认情况下,计数器的阈值并不是绝对次数,如果是绝对次数,在运行一段时间后,所有的代码理论上都会被JIT编译器编译.
所以,此阈值指定的是某一段时间内调用的方法次数,超过此时间,则此方法的方法计数器减少一半调用次数,这个过程称为衰减,这一段时间称为半衰周期
可以通过虚拟机参数 -XX: -UseCounterDecay关闭热度衰减功能.
也可以通过 -XX:CounterHalfLifeTime设置半衰周期
5.2 设置程序执行方式
默认情况下,HotSpot是解释器和编译器并存的方式,但是也可以通过命令指定HotSpot使用某一种
-Xint: 完全采用解释器
-Xcomp: 完全采用即时编译器
-Xmixed: 混合模式 默认
默认情况下 mixed
命令行设置模式
# 解释器java -Xint-version# 编译器java -Xcomp-version
5.3 C1和C2编译器
JIT细分可以分为C1和C2两种编译器
- C1是Client编译器,对字节码进行简单可靠的优化,耗时短
- C2是Server编译器,对字节码进行耗时较长的优化,以及激进优化,优化后的代码效率更高
通过参数 -clinet 和 -server指定编译器
64位jdk默认是-server模式,所以默认使用了C2编译器
5.3.1 C1和C2的优化策略
C1:
- 方法内联: 将引用的方法代码编译到引用点处,减少栈帧的生成,减少参数传递以及跳转过程
- 去虚拟化: 如果一个接口只有一个实现类,就减少内联.
- 冗余消除: 在运行期间将一些不会执行的代码折叠掉
C2: 基于逃逸分析,可以看下堆那一节的逃逸分析
- 栈上分配
- 变量替换
- 同步消除
但是并不是-Server就会全部使用C2编译器,而是开启了分层策略,基于性能监控的情况下,首先使用C1,根据性能监控情况使用C2
可以理解为 -client 只会使用C1
-server 是C1和C2都会使用.
5.4 总结
JIT编译器编译出来的机器码的性能都比解释器高.
C2比C1慢,但是再运行一段时间后,C2的速度远高于C1
6. 扩展: Graal编译器和AOT编译器
jdk10开始JIT即时编译器有一个新的编译器 Graal编译器
可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler开启该编译器
jdk9开始新增AOT编译器 Ahead Of Time提前编译器
这个编译器是跟JIT对立的,借助了Graal编译器,在程序运行之前就将字节码编译成机器指令.
好处: 提前编译成机器指令,加快了执行的速度,不存在第一次运行慢的问题
坏处:
- 提前编译成机器指令,java的跨平台性就受到了影响,因为不同平台的机器指令是不一样的
- 降低java的动态性,提前编译,一些只能在java运行中动态生成的特性就被压缩了
- 目前只支持Linux x64 java base