一、概述
1. 地位
- Java虚拟机核心组成部分之一。
2. 虚拟机与物理机
- 【虚拟机】是一个相对于【物理机】的概念,两种机器都有代码执行的能力。
- 【物理机】的执行引擎,直接建立于处理器、缓存、指令集和操作系统层面。
- 【虚拟机】的执行引擎由虚拟机程序实现。可以不受物理条件制约地定制指令集与执行引擎结构体系,能执行哪些不被硬件直接支持的指令集格式。
3. 主要职责
- 将字节码指令解释 / 编译为平台上的本地机器指令。
二、Java代码编译和执行过程
1. 解释器与JIT编译器
1) 解释器
- Java虚拟机中,对字节码采取逐行解释,翻译为平台机器指令并执行的软件模块。
2) JIT编译器(Just In Time Compiler)
- 将字节码编译为本地平台相关的机器语言的软件模块。
2. 半编译半解释型语言
- JDK 1.0时代,Java语言定位为解释执行的语言。
- 目前的JVM执行Java代码,将解释执行和编译执行结合起来。
三、机器码、指令、汇编语言
1. 机器码
- 采用二进制方式编码的指令。
- 与CPU硬件紧密相关。
- 与其他类型语言相比,执行速度最快。
- 容易出错,不利于理解和记忆。
2. 指令与指令集
1) 指令
- 基于机器码可读性差的优化。
- 0和1的不同序列简化为指令,如mov、inc等。
- 不同的硬件平台可能不同。
2) 指令集
- 不同硬件平台,各自支持的指令有差别。区分为平台指令集。
- x86指令集,对应x86架构平台。
- ARM指令集,对应ARM架构平台。
3. 汇编语言
- 基于指令可读性差的优化发明。
- 汇编语言中,用助记符(Mnemonic)代替机器指令操作码,用地址符号(symbol)或标号(Label)代替指令或操作数地址。
- 平台 相关性。在不同硬件平台的汇编语言对应不同的机器语言指令集。
4. 字节码
1) 概述
- 一种中间状态的二进制代码,比机器码更抽象。需要直译器转译后才能成为机器码。
2) 目的
- 为了实现特定软件环境和软件运行,与硬件无关。
3) 实现
通过前端编译器(字节码编译器)和虚拟机器(如JVM)。
- 前端编译器:编译为字节码。
- 虚拟机器: 转译为可以直接执行的指令。
典型应用是Java bytecode。
四、解释器
1. 分类
1) 字节码解释器
- 纯软件代码模拟字节码执行,效率低下。
2) 模板解释器
- 每一条字节码和一个模板函数关联,模板函数直接产生该字节码执行时的机器码,很大程度上提高解释器性能。
2. Hotspot VM中的解释器
主要是Interpreter + Code两个模块构成。
1) Interpreter模块
解释器核心功能。
2) Code模块
管理运行时生成的机器指令。
3. 现状
- 解释器实现非常简单。Java,Perl,Python,Ruby都依赖于解释器。
- 低效的代名词。
- JVM平台提出JIT即时编译器,优化低效的问题。
- 为中间语言的发展做出了贡献。
五、JIT编译器
Java程序运行性能已经脱胎换骨,能达到和C,C++一较高下的地步。
1. Hotspot VM为什么考虑解释器与JIT编译器并存的架构?
1) 概括说明
互相协作,取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接翻译执行本地代码的时间。
- 响应速度快:
- 程序启动后,解释器可立马发挥作用,省去编译时间。
- 而编译器必须把字节码编译为本地代码,需要一定的执行时间。编译完成后才是执行快的时候。
- 场景权衡。
- JRockit执行性能会非常高效,但启动时候必然花费更长时间。
- 对于看重启动时间的应用场景而言,解释器与编译器并存的架构换取平衡点。
- 解释执行在编译器激进优化不成立时,解释器作为编译器的逃生门。
2) 案例——阿里团队
- 概括说明
- 解释器与编译器在执行线上环境微妙的辩证关系:
- 机器在热机状态能承受的负载要大于冷机状态。
- 生产环境发布过程中,以分批的方式进行发布,根据机器数划分为多个批次,每个批次机器数之多占到整个集群的 1 / 8。
- 解释器与编译器在执行线上环境微妙的辩证关系:
- 案例详情
某位程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分两批发布。
如果是热机状态,正常情况下一般机器勉强可以承载流量,但由于刚启动的JVM均是解释执行,还未进行热点代码统计和JIT动态编译,当前发布成功的服务器马上宕机。
此故障某种程度上说明了JIT的存在。
2. JIT编译器介绍
1) 编译器的三种可能解释
- 前端编译器
- java文件 => class文件
- 后端运行期编译器(JIT编译器)
- 字节码文件 => 机器码
- 静态提前编译器(Ahead Of Time Compiler)
- .java文件 => 机器码
2) 热点代码及探测方式
- 热点代码
- 是否需要JIT编译,取决于代码被调用执行的频率。
- 频繁被调用的【热点代码】,被直接编译为本地平台机器指令,以此提升Java程序的性能。
- 发生在方法执行过程中,因此也被称为栈上替换(OSR, On Stack Replacement)
- 基于计数器的热点探测。
JVM为每一个方法建立两个不同类型的计数器:- 方法调用计数器(Invocation Counter)
统计被调用次数,- VM参数-XX:CompileThreshold
- Client模式:默认1500次
- Server模式:默认10000次
- 回边计数器(Back Edge Counter)
统计循环体内的循环次数
- 方法调用计数器(Invocation Counter)
- 方法调用计数器 ==> 热点代码编译判断
- 方法调用计数器 ==> 热度衰减
- 不做任何设置,方法调用计数器的统计的是一段时间内方法被调用的次数。
- 当超过一定的时间限度,仍未达到编译判断阈值,则该统计值减小一半,即热度的衰减(Counter Decay)。这段时间即称为半衰周期(Counter Half Life Time)。
- 半衰周期伴随GC过程进行。
- 热度衰减开关:-XX:-UseCounterDecay
- 半衰周期参数:-XX:CounterHalfListTime
- 回边计数器
4) JIT的服务器与客户端
Hotspot VM内嵌两个JIT编译器:
JIT编译器 | 别名 | 特点 | VM参数设置 |
---|---|---|---|
Client编译器 | C1 | 1) 进行简单可靠的优化,耗时短; 2) 偏向编译速度考虑; |
-client |
Server编译器 | C2 | 1) 执行耗时较长的优化,以及激进优化 2) 偏向代码执行效率的考虑; |
-server |
主要优化策略:
- Client编译器(C1编译器)
- 方法内联:将引用的函数代码编译到引用点处,以减少栈帧生成,从而减少参数传递及跳转过程。
- 去虚拟化:对唯一实现类进行内联。
- 冗余消除:运行期间不会执行的代码进行折叠。
- Server编译器(C2编译器)=> 主要是全局层面,逃逸分析是优化的基础
- 标量替换:用标量值代替聚合对象的属性值。
- 栈上分配:对未逃逸的对象分配在栈空间。
- 同步消除:清楚同步操作,通常指synchronized。
- 分层编译策略(Tiered Compilation)
不开启性能监控,解释执行触发C1编译,可以进行简单优化。开启性能监控后,C2编译会根据性能监控信息,以进行激进优化。
Java 7之后,-server模式会默认开启分层编译策略。
5) JIT编译器小结
- JIT编译出的机器码性能比解释器高。
- 对于C1编译器而言,C2编译器启动时长比较慢,但系统稳定后,C2编译器执行速度远快于C1编译器。
6) JIT编译器扩展
- Graal编译器
- JDK 10起,引入新的编译器Graal。效果已经追平C2编译器,未来可期。
- 目前Graal编译器带着【实验状态】标签,VM参数为
-XX:+UnlockExperimentalVmOptions -XX:+UseJVMCICompiler
六、AOT编译器
1. AOT概念说明
AOT编译,于JIT是对立的概念:
- JIT编译:在程序运行过程中,即时编译为机器码。
- AOT编译:在程序运行之前,转换为机器码。
- 优势:减轻给人们带来的第一次运行慢的不良体验。
- 劣势:
- 破环了【一次编译,到处运行】。必须为不同硬件、OS编译对应的发行包。
- 降低Java链接过程的动态性。代码在编译器必须全部已知。
- 最初只支持Linux x64 java base
2. AOT编译器及相关工具
- JDK 9 引入静态提前编译器(Ahead Of Time Compiler)
- Java 9 引入了实验性AOT编译工具jaotc。它借助了Graal编译器,将类文件转换为机器码,存放于生成的动态共享库。