后端编译器
即时编译器
什么是即时编译器?
前提:
- 虚拟机规范中并没有强制要求必须实现即时编译器,虚拟机可以采用纯解释执行
- 但是 [即时编译器性能的好坏、对于代码优化质量的高低]{.blue} 却是衡量虚拟机是否优秀的关键标准
名称:即时编译器(Just In Time 编译器)
定义:[负责在 进程运行期间 将 热点探测 决定的字节码指令 编译并且优化 成本地机器指令]{.red}
分类:
客户端编译器:
- 名称:Client Compiler(C1 编译器)
- 特点:[采用的代码优化策略相对简单可靠,确保客户端的编译速度]{.red}
服务器端编译器:
- 名称:Server Compiler(C2 编译器)
- 特点:[采用的代码优化策略更加的激进,全局性的优化,服务器端的执行速度也更快]{.red}
:::info
① 采用的优化策略更多,显然编译器执行的编译过程就更长,显然编译速度就会更慢
② 只要编译期间采用的优化策略越多,代码质量就会越好,执行速度就会越快
:::
Graal 编译器:JDK 10 之后提供的编译器,目的是替代服务器端编译器
细节:
- [虚拟机预热:]{.red}
- 虚拟机刚启动时只采用解释执行,热点探测本质就是统计方法的调用次数,即时编译器无法立刻生效
- [虚拟机在等待即时编译器生效的这段时间称为虚拟机预热]{.blue}
- [虚拟机假死:]{.red}
- 虚拟机预热完成之前都称为冷机状态,虚拟机长时间运行之后称为热机状态
- [虚拟机的热机状态所能够承受的负载显然要大于虚拟机的冷机状态]{.blue}
- [如果将处于热机状态的虚拟机的部分任务分配给处于冷机状态的虚拟机,就会导致其无法承受过大的流量而假死;毕竟虚拟机刚开始只采用解释执行,突然将大流量的任务交给处于冷机状态的虚拟机,肯定难以承受]{.green}
- [虚拟机预热:]{.red}
解释器和即时编译器如何配合执行?
:::primary
参考博客:jvm分层编译级别
:::
解释模式:
定义:仅使用解释器执行代码的模式
设置解释模式命令:[-Xint]{.blue}
编译模式
定义:仅使用即时编译器执行的模式
设置编译模式命令:[-Xcomp]{.blue}
细节:[在即时编译器无法执行的情况下,解释器会强制介入]{.red}
混合模式
定义:解释器和即时编译器混合执行的模式
设置混合模式命令:[-Xmixed]{.blue}
- [HotSpot 虚拟机默认采用混合模式]{.red}
常规混合模式:[解释器配合任意一个即时编译器进行工作]{.red}
分层编译模式:
前提:即时编译器想要完成更好的优化,就可能需要解释器的介入,替他监控并收集相关信息
开启分层编译策略:
- 命令:[-XX:+TieredCompilation 开启分层策略 / -XX:-TieredCompilation 关闭分层策略]{.blue}
- JDK 6 分层编译策略被初步实现,JDK 7 之后分层编译才作为默认策略被开启
分层:
第 0 层(解释执行):程序纯解释执行,并且不开启性能监控功能
第 1 层(简单 C1 编译):开启客户端编译器进行代码优化,解释器仍然不开启性能监控功能
!!不开启性能监控怎么知道哪些代码是热点代码啊?!!{.bulr}
第 2 层(受限 C1 编译):继续使用客户端编译器进行代码优化,解释器开启 [部分性能监控功能]{.red}
- 方法调用计数器和回边计数器等少量性能监控功能
第 3 层(完全 C1 编译):继续使用客户端编译器进行代码优化,解释器开启 [全部性能监控功能]{.red}
- 新增分支跳转、虚方法调用版本等信息的收集
- [大多数方法第一次被编译就是采用第 3 层级别的编译]{.red}
第 4 层(C2 编译):开启服务器端编译器进行代码优化,将会采取更加激进的优化策略
- [服务器端编译器较忙的情况下会将方法下放到第 2 层级别的编译]{.red}
- [等到服务器端编译器不太忙的时候,再对此前下放的方法重新编译]{.red}
核心:
- 执行频率较高的代码优先被C1编译器编译
- 随着时间的推移再采用C2编译器做进一步优化编译,此前的本地代码被抛弃
细节:启用分层编译之后,不再是单独使用任何一个即时编译器,而是交替使用,同时使用
速率测试:
测试代码
public static void main(String[] args)
{
long start = System.currentTimeMillis();
testMethod(10000000);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 计算质数的方法
public static void testMethod(int count)
{
System.out.println("开始执行...");
// 没有经过任何优化的算法采用纯解释执行实在是太他妈的慢了
for (int i = 3; i < count; i++){
for (int j = 2; j < Math.sqrt(i); j++){
if (i % j == 0)
break;
}
}
System.out.println("执行结束...");
}测试结果
- 纯解释执行的效率显然是非常低下的,尤其是在算法没有优化且计算量大的情况下,非常糟糕
- 纯编译执行效果显然是非常好的,即使算法写的不好,虚拟机也会采用相应的优化策略,提高效率
- 混合执行效果也不错,但是相比于纯编译似乎没有多大的提升,甚至有点下降
- 这是你可能产生了一个疑问,既然纯编译效果这么好,那为什么还要留着解释器呢?接下来就会提到这个问题
// 解释器和编译器配合模式测试
// 纯解释执行 36980
// 纯编译执行 4902
// 混合执行 5031
运行模式
- 服务器端:[没有分层编译的混合模式中默认使用服务器端即时编译器]{.red}
- 设置命令:[-server]{.blue}
- 客户端:[没有分层编译的混合模式默认使用客户端即时编译器]{.red}
- 设置命令:[client]{.blue}
- 细节:虚拟机根据宿主机器选择运行模式,不过在 [64位的虚拟机]{.blue} 中默认采用服务器端运行
- 服务器端:[没有分层编译的混合模式中默认使用服务器端即时编译器]{.red}
为什么要采用解释器和即时编译器并存的架构呢?
回顾:
- 主流的 HotSpot、OpenJ9 虚拟机都采用解释器和即时编译并存的架构
- 远古时代的 Classsic 虚拟机仅采用解释执行的架构
- BEA 开发的 JRockit 虚拟机仅采用编译执行的架构
原因:
- Java 程序不仅需要运行在服务器端也需要运行在客户端
- [客户端程序更加关心程序的启动速度,对运行速率没有太高要求,过长的编译期会严重影响客户端的启动]{.red}
- [解释器不需要对源码执行编译过程所以启动速度非常快,也不需要和具体的硬件产生关系]{.blue}
- [服务器端则更加关心其运行速度,因为需要频繁处理客户端的请求,所以需要更加全局性的代码优化策略]{.red}
- 但是解释器每次都需要向处理器解释源码的含义,对于长时间运行的服务器来说效率非常差
- 所以即时编译器就用来解决运行时的执行效率问题
- [即时编译器会采用热点探测,对频繁执行的代码进行编译,再次执行该代码时就会提高其运行效率]{.blue}
核心:[虚拟机能够同时兼具启动速度和执行速度两种优势,能够适应不同进程情况]{.red}
细节:
- [即时编译器和解释器是异步执行:代码在编译的过程中,同时进入解释器执行,等到再次调用时再编译执行]{.red}
- [解释器还可以作为即时编译器激进优化的逃生门]{.red}
:::info
① 即时编译器有时候会采用非常激进的优化策略,这些优化策略并不是一定都能够成功的,是由一定概率的
② 如果优化策略没有成功,那么代码就会从编译执行退回到解释执行
:::

如何触发即时编译?
热点代码(HotSpot Code)
- 定义:[被多次调用的 方法 或者被多次执行的 循环体]{.red}
- 多次调用?多次执行?到底是多少次呢?有没有一个定量呢?
- 替换方式:
- 普通方式:
- 内容:方法下次调用时再使用编译好的本地代码
- 特点:适用于方法调用计数器
- 栈上替换(On Stack Replacement: OSR):
- 内容:[循环体代码将在解释执行的过程中被编译生成的本地代码替换]{.red}
- 特点:适用于回边计数器
- 普通方式:
- 细节:[无论是多次调用的方法还是循环体,编译的对象都是整个方法]{.red}
- 定义:[被多次调用的 方法 或者被多次执行的 循环体]{.red}
热点探测(HotSpot Code Detection)
定义:探测执行的代码是否为热点代码
方式:
- 基于采样的热点探测
- 内容:[虚拟机将会 周期性 的检查各个线程的调用栈顶,频繁出现在栈顶的方法被认为是热点代码]{.red}
- 特点:
- [实现简单且高效,可以得知方法之间的调用关系]{.red}(不太理解后面这个优点)
- [难以精确确定方法的热度,而且由于是周期性的,容易受到线程阻塞的影响]{.green}
- 基于计数器的热点探测
- 内容:[虚拟机将会为每个方法建立 计数器,统计执行的次数,超过阈值的方法就认为是热点代码]{.red}
- 分类:[方法调用计数器、回边计数器]{.red}
- 特点:
- [能够精确测定方法的热度,并且可以使用热度衰减技术]{.red}
- [为每个方法都维护计数器显然比较麻烦,并且无法得知方法之间的调用关系]{.green}
- 基于采样的热点探测
计数器:
方法调用计数器
作用:统计一个方法被执行的次数,[方法调用计数器和回边计数器之和]{.red} 超过阈值之后发起编译请求
阈值:
- 默认阈值:客户端默认值为 1500 次,服务器端默认值为 10000 次
- 设置阈值的命令:[-XX:CompileThreshold=threshold]{.blue}
过程
回边计数器
作用:统计一个方法中循环体被执行的次数
[方法调用计数器和回边计数器之和]{.red} 超过阈值之后发起 [OSR 编译请求]{.red}
阈值:
- 客户端默认阈值:[方法调用计数器默认值 x OSR 默认比率 / 100]{.blue} 默认值 13995
- [方法调用计数器的值(-XX:CompileThreshold):默认值 1500]{.blue}
- [OSR 比率(-XX:OnStackReplacePercengtage):默认值 933]{.blue}
- 服务器端默认值:[方法调用计数器默认值 x (OSR 比率 - 解释器监控比率) / 100]{.blue} 默认值 10700
- [OSR 比率(-XX:OnStackReplacePercengtage):默认值 140]{.blue}
- [解释器监控比率(-XX:InterperterProfilePercentage): 默认值 33]{.blue}
- 客户端默认阈值:[方法调用计数器默认值 x OSR 默认比率 / 100]{.blue} 默认值 13995
过程:
细节:J9 曾采用基于采样的热点探测,HotSpot 采用基于计数器的热点探测
热度衰减(Counter Decay)
- 前提:仅有基于计数器的热点探测中的方法计数器可以使用热端衰减,回边计数器无法使用
- 定义:[周期时间内某个方法始终没有达到方法调用计数器的阈值,那么该方法的方法调用计数器的值减半]{.red}
- 周期时间:《深入理解虚拟机》中没有明确指明默认时间到底是多久,只说了这段时间又称为半衰周期
- 命令:
- [-XX:+UseCounterDecay 开启热度衰减 / -XX:-UseCounterDecay 关闭热度衰减]{.blue}
- [-XX:CounterHalfLifeTime=time 设置半衰周期的时间,单位是秒]{.blue}
即时编译如何进行的?
即时编译会采用哪些优化策略呢?
- 核心:[不同的即时编译器采用的优化策略也不尽相同]{.red}
- C1 编译器:
- 方法内联
- 去虚拟化
- 冗余访问消除
- 复写传播
- C2 编译器:[逃逸分析是 C2 编译器进行优化的前提]{.red}
- 栈上分配
- 标量替换
- 同步消除