后端编译与优化
1、概述
字节码看作程序语言的一种中间表示形式,那么编译器无论在何时,何种状态下把Class文件转化成与本地基础设施相关的二进制机器码,都可以视为整个编译器的后端。
提前编译器或即时编译器都不是Java虚拟机必需组成部分。
后端编译器性能的好坏,代码优化质量的高低确实衡量一款商用虚拟机优秀与否的关键指标之一;
2、即时编译器(JIT)
- 当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高效率,虚拟机就会把这些代码编译成本地机器码,并以各种手段尽可能进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
- 为何HotSpot虚拟机要是有解释器与即时编译器并存的架构?
- 为何HotSpot要实现两个或三个不同的即时编译器?
- 程序何时使用解释器执行?何时使用编译器执行?
- 哪些程序会被编译成本地代码?如何编译本地代码?
- 如何从外部观察到即时编译器的编译过程和编译结果?
1、解释器与编译器
- 程序执行后,解释器可以立即执行,省去编译的时间,立即执行;
- 随着程序的执行,编译器发挥作用,将频繁执行的代码编译成本地代码,检查解释器的中间损耗,获取更多的执行效率;
- 解释器还可以编译器激进优化的逃生门;
以下HotSpot为例:
- 客户端编译器(C1)、服务端编译器(C2) 、Graal编译器(代替C2,处于实验状态);
- 可以使用**-client 或-server参数去强制指定虚拟机运行客户端模式还是服务端模式**;
- -Xint 强制解释模式
- -Xcomp 强制编译模式
- 解释器与编译器并行,混合模式
- JDK7的服务端模式虚拟机中作为(分层编译)默认编译器策略被开启;(转)分层编译详解
2、编译对象与触发条件
- 热点代码:被多次调用的方法体,被多次执行的循环体
- 编译的目标对象都是整个方法体;
- 栈上替换:方法的栈帧还在栈上,方法就被替换了;
- Java虚拟机是如何统计某个方法或某段代码被执行过多少次的呢?
- 基于采样的热点探测 (虚拟机周期性检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”);
- 基于计数器的热点探测 (虚拟机会为每个方法(甚至是代码块)建立计数器,统计次数,超过阈值就认为是热点方法);
- 方法调用计数器
- 客户端默认1500次、服务端默认10000次
- -XX:ComplieThreshold 人为设定
- 统计相对次数,超过一定时间限度,不足于提交编译器编译,会被衰减一半;即方法调用计数器热度的衰减;(-XX:UseCounterDecay关闭热度衰减,-XX:CounterHalfLifeTime:设置半衰周期的时间)
- 回边计数器(循环边界往回跳转)(目的为了触发栈上替换编译)
- 虚拟机运行在客户端模式下,回边计数器阈值计算公式为:方法调用计数器阈值(-XX:CompileThreshold)乘以OSR比率(-XX:OnStackReplacePercentage)除以100。
其中**-XX:OnStackReplacePercentage默认值为933**,如果都取默认值,那客户端模式虚拟机的回边计数器的阈值为13995。 - 虚拟机运行在服务端模式下,回边计数器阈值的计算公式为:方法调用 计数器阈值(-XX:CompileThreshold)乘以(OSR比率(-XX:OnStackReplacePercentage)减去解释器监控比率(-XX:InterpreterProfilePercentage)的差值)除以100。
其中**-XX:OnStackReplacePercentage默认值为140,-XX:InterpreterProfilePercentage默认值为33**,如果都取默认值,那服务端模式虚拟机回边计数器的阈值为10700。
- 虚拟机运行在客户端模式下,回边计数器阈值计算公式为:方法调用计数器阈值(-XX:CompileThreshold)乘以OSR比率(-XX:OnStackReplacePercentage)除以100。
3、编译过程
- -XX:-BackgroundCompilation来禁止后台编译,后台编译被禁止后,当达到触发即时编译的条件时,执行线程向虚拟机提交即时编译请求以后就会阻塞等待,直到编译过程完成再开始执行编译器输出的本地代码;
- 即时编译的标准来看:服务端编译器比较缓慢,但是它的编译速度依然远远超过传统的的静态优化编译器、而且它相对于客户端编译器编译输出的代码质量有很大提高,可以大幅度少本地代码的执行时间,从而抵消了额外的编译时间开销,索引有很多非服务端的应用选择使用服务端模式的HotSpot虚拟机来运行;
3、提前编译器(AOT)
- 即时编译器消耗的时间都是员额不能可用于程序运行的时间,消耗的资源都是原本可以用于程序运行的资源,这个约束从未减弱,更不会消失;
- 提前编译:本质上给即时编译器做缓存加速,改善Java程序的启动时间,以及需要一段时间预热后才能达到最高性能的问题。这种提前编译被称为动态提前编译或即时编译缓存;
- Jaotc提前编译器(openJDK/OracleJDK),基于Graal编译器实现的新工具
- 三种即时编译器相对于提前编译器的天然优势:
- 性能分析制导优化
- 激进预测优化
- 链接时优化
4、编译器优化技术
编译器的目标是做由程序代码翻译为本地机器码的工作;
- 最重要的优化技术之一:方法内联
- 把目标方法的代码原封不动地复制到发起调用的方法之中;
- 最前沿的优化技术之一:逃逸分析
- 分析对象动态作用域,当一个对象在方法里面被定义后,他又可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸
- 从不逃逸、方法逃逸到线程逃逸称为对象由低到高的不同逃逸程度
- 栈上分配
- 支持方法逃逸、但不能支持线程逃逸
- 标量替换
- 如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复到原始类型来访问,这个过程被称为标量替换;- 不允许对象逃逸出方法范围内;
- 同步消除
- 语言无关的经典优化技术之一:公共子表达式消除
- 如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量值都没有发生变化,那么E的这次出现就是公共子表达式;直接使用前面计算过的表达式代替E就是公共子消除;
- 语言相关的经典优化技术之一:数组边界检查消除
参考书籍: 《深入理解Java虚拟机》第三版