Execution Engine
- 虚拟机与物理机的对比
- 物理机上的执行引擎是直接建立在处理器, 缓存, 指令集(如 x86架构的指令集, ARM架构的指令集)和操作系统层面上的, 而虚拟机的执行引擎是由软件自行实现的, 因此不受物理条件制约的定制指令集, 并能够执行那些不被硬件直接支持的指令集格式
执行引擎概述
- class文件的字节码指令并非等价于本地机器指令, 它是无法直接运行在操作系统上的, 需先使用 JVM的类加载器加载到内存, 再通过执行引擎读取并执行的
- 执行引擎的解释器依赖于 PC寄存器. 每当执行完一项指令后,PC寄存器就会更新下一条, 将被执行的指令地址
- 执行引擎可以通过局部变量表中的对象引用准确定位到堆中的对象实例, 以及通过对象头中的元数据指针定位目标对象的类型信息等
- Hotspot VM的执行引擎采用解释器与即时编译器并存的架构(Java语言是一种半编译和半解释型语言)
编译分为前/后端
- 前端编译是 Java文件转变成 .class文件的过程(
如 javac, Eclipse JDT(Java Development Tools)中的增量式编译器 (ECJ)
) - 后端编译是字节码转变成(经过汇编)机器码的过程, 即时编译器(Just In Time Compiler, JIT编译器)
内部结构
- 包含3部分: 解释器(Interpreter), 即时编译器(JIT Compiler), 垃圾回收器(Garbage Collection)
解释器(Interpreter)
- 解释器对字节码是逐行解释的, 所以响应速度块. 如 JVM应用启动时, 就会使用解释器解释字节码
- 解释器依赖于 PC寄存器. 每当执行完一项指令后,PC寄存器就会更新下一条, 将被执行的指令地址
- 使用解释器的高级语言有 如 Python, Perl, Ruby等
即时编译器(JIT Compiler)
- 即时编译器的作用是即时的将热点代码(如 函数体)直接编译成(经过汇编)本地平台机器码, 并缓存到方法区里
- 首次缓存时, 相应速度会受影响, 但缓存后由于无需再重复做字节码与机器码的转换操作, 直接执行缓存好的机器码, 所以执行效率会大幅度上升
即时编译的执行条件
-
代码被即时编译的条件是根据代码被调用的频率而定. 如 方法被调用的次数或方法内的循环体的回边次数 达到阈值时, 这类代码称之为热点代码(Hot Spot Code), 由于这种编译方式发生在方法的执行过程中, 因此又称为 OSR(On Stack Replacement, 栈上替换)编译
-
在 Hotspot VM中成为热点代码的条件是基于计数器的热点探测, 有2种类型计数器:
-
(1) 方法调用计数器(Invocation Counter)记录方法调用次数:
方法调用过程: 1. 是否已编译过, 2. 没有就递增调用次数, 再判断是否达到编译数. 符合条件就编译, 否则解释执行
client模式下默认调用次数为 1500
server模式下默认调用次数为 10000次, 超过了, 就会触发 JIT编译
- 显式调整调用次数:
-XX:CompileThreshold=N
默认情况下, 被调用的次数不是绝对次数, 而是指定时间段(Counter Decay, 热度衰减)的执行频率, 如果超过了指定的时间段, 同时被调用次数仍不足以让它提交给即时编译器编译, 此时方法的调用次数会被消减一半. 这段时间称之为半衰周期(Counter Half Life Time), 可以通过参数:-XX:CounterHalfLifeTime=N 单位为秒
调整半衰周期(此参貌似只能在调试时可用)- 热度衰减的动作是虚拟机进行垃圾收集时顺便进行的, 可以通过参数关闭热度衰减:
-XX:-UseCounterDecay
. 关闭后, 被调用的次数就成为绝对次数, 这样只要系统运行足够长, 绝大部分的热点代码都会被编译成本地代码并缓存
- (2) 回边计数器(Back Edge Counter)统计循环体的回边次数:
方法中循环体代码的执行次数, 准确地说, 是回边次数而不是循环次数, 因为并非所有的循环都会回边, 如空循环属自己跳转到自己的过程, 不会被回边计数器统计.
client模式下默认回边次数为 13995
server模式下默认回边次数为 10700
- client模式调整回边计数器的阈值公式为:
方法调用计数器阈值(CompileThreshold)xOSR比率(OnStackReplacePercentage默认值为933)/100
- server模式调整回边计数器的阈值公式为:
方法调用计数器阈值(CompileThreshold)x(OSR比率(OnStackReplacePercentage默认值为140)-解释器监控比率(InterpreterProfilePercentage默认值为33)/100
Hotspot VM内嵌两种 JIT编译器
- (1) Client Compiler(C1):
特点是对字节码进行简单优化, 耗时短, 编译速度快
- 优化策略:
- 方法内联: 将引用的函数代码编译到引用点处, 这样可以减少栈帧的生成,减少参数传递以及跳转过程
- 去虚拟化: 对唯一的实现类进行内联
- 沉余消除: 在运行期间把一些不会执行的代码折叠掉
- (2) Server Compiler(C2):
特点是激进优化(编译时间较长). 但优化的代码执行效率更高
- 优化策略:
- 标量替换: 用标量值代替聚合对象的属性值
- 栈上分配: 将未逃逸的对象分配到栈上
- 同步消除: 消除同步操作, 通常指 synchronized
- C2编译器启动时长比 C1编译器慢, 但是系统稳定启动后, C2编译器执行速度远远快于 C1编译器
* 分层编译策略: 解释执行时不开启性能监控, 则会触发 C1编译. 如果开启了, C2编译器会根据性能监控信息进行激进优化.
Jdk7开始服务端模式会默认开启分层编译策略: -XX:+TieredCompilation
选择模式
* 查看当前模式: java -version
-Xint
只采用解释器模式执行程序-Xcomp
只采用编译器模式执行程序, 如果即时编译出了问题, 解释器会介入执行-Xmixed
采用解释器和即时编译器的混合模式共同执行程序(默认)
Jdk9引入了 AOT编译器(Ahead Of Time Compiler, 静态提前编译器), 所谓 AOT编译是与即时编译相对立的. 即时编译是在程序的运行过程中将字节码转换为机器码, 而 AOT编译是在程序运行之前, 便将字节码转换为机器码的
- 特点: 加载时已经预编译成二进制了, 所以可以直接执行, 不存在即时编译的预热, 从而改善了 Java应用给人带来的第一次运行慢的问题
- 缺点: 必须为每个不同硬件, OS编译对应的发行包. 降低了 Java链接过程的动态性, 加载的代码在编译期就必须全部已知
Jdk10开始新引进了全新的即时编译器 Graal编译器. 目前与C2编译器, 效率相当. 需通过开关参数激活使用
-XX:+UnlockExperimentalVMOptions
开启允许使用实验性参数-XX:+UseJVMCICompiler
启用 Graal编译器来替换 C2编译器
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!