- 编译器无论在何时、何种状态下将Class文件转换成与本地基础设施相关的二进制机器码,它都可以视为整个编译过程的后端。
- 即时编译一直是绝对主流的编译形式,不过提前编译也逐渐被主流JDK支持。
1 即时编译器
- 目前两款主流的Java虚拟机(HotSpot、OpenJ9里面),Java程序都是通过解释器进行解释执行的。当虚拟机认为某个方法或代码块的运行特别频繁,就会把这些代码编译成本地机器码,运行时完成这个任务的编译器叫作即时编译器。
1.1 解释器与编译器
- 目前主流的Java虚拟机都采用了解释器与编译器并存的运行架构。
- 当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立刻运行。当程序启动后,随着时间地推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器中间的损耗。
- HotSpot虚拟机内置了两个(或三个)即时编译器,其中两个编译器存在以及。分别是
客户端编译器(C1编译器)
和服务端编译器(C2编译器)
,第三个是JDk10时出现,为了代替C2的Graal编译器
。 - 为了在程序启动相应速度与运行效率之间达到最佳平衡状态,HotSpot虚拟机在编译子系统中加入了分层编译的功能。
分层编译的好处是什么?
- 实施分层编译后,解释器、客户端编译器、服务端编译器就会同时工作,热点代码可能被多次编译。
- 用客户端编译器可以获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无须承担收集性能监控信息的任务。
2 编译对象与触发条件
- 本章概述中提到了在运行过程中会被即时编译器编译的目标是"热点代码",这里所指的热点代码主要有两类。
- 被多次调用的方法
- 被多次调用的循环体
- 对于这两种情况,编译的目标对象都会是整个方法体,而不是单独的循环体。
3 编译过程
-
无论是方法调用产生的标准编译请求,还是栈上替换编译请求。虚拟机在编译器还未完成编译之前,都将按照解释方法继续执行代码。
-
而编译动作会进行在后台的编译线程中。
-
在后台编译执行的过程中,服务端编译器和客户端编译器的编译过程是有差别的。
-
客户端编译器
-
它是一个相对简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。
-
服务端编译器
-
它是一个专门面向服务端典型用场景的、为服务端的性能配置针对性调整过 的 编译器。
2 提前编译器
- 提前编译的发展,直到在Android的世界里,使用了提前编译的ART,ART一诞生就把使用即时编译器的Dalvik虚拟机按在地上使劲蹂躏。
提前编译的优劣得失?
- 优点
- 提前编译没有执行时间和资源限制的压力,能够毫无顾虑地使用重负载优化手段。
- 缺点
- 最传统的提前编译应用形式,就是在程序运行之前把程序代码编译成机器码的静态翻译工作。但是它会占用程序运行时间和运算资源。
- 提前编译的第二条途径,就是将编译工作提前做好后保存下来,下次运行到这块代码的时候,就直接加载进来使用。但是这种即时编译缓存输出的代码质量反而要低于即时编译器。
3 编译器优化技术
- 编译器的目标就是由程序代码翻译成本地机器码的工作
- 但是它的难点不在于能不能成功翻译出机器码,而是输出代码优化质量的高低。
- 即时编译器优化技术的数量有很多,接下来会介绍几种常用的优化技术。并且通过Java代码变化来展示,不过即时编译器对这些代码优化变化是建立在
代码的中间表示或者是机器码之上的。
基础案例
package Compilation;
public class BasicDemo {
B b = new B();
int y , z , sum;
static class B{
int value;
final int get(){
return value;
}
}
public void foo(){
y = b.get();
//...
z = b.get();
sum = y + z;
}
}
- 方法内联
- 先采用方法内联。主要目的有两个,一个是去除方法调用的成本(如查找方法版本、建立栈帧等);二是为其他优化建立良好的基础。(编译器一般会把内联优化放在优化序列最靠前的位置)
public void foo(){
y = b.value;
//...
z = b.value;
sum = y + z;
}
- 冗杂访问消除
- 假设代码中间的省略号所代表的操作,不会改变b.value的值。
- 那么就可以把z = b.value 变成 z = y。(就可以不再去访问对象b的局部变量)
- 如果把b.value看成是一个表达式,也可以看成一种公共子表达式消除。
//冗杂访问消除
public void foo(){
y = b.value;
//...
z = y;
sum = y + z;
}
- 复写传播
- 逻辑中没有必要使用额外的变量z,它与变量y完全相同,所以可以用y来代替z。
//复写传播
public void foo(){
y = b.value;
//...
y = y;
sum = y + y;
}
- 无用代码消除
- 完全没有意义的代码或者是永远不会被执行的代码也可以被删除。
public void foo(){
y = b.value;
//...
sum = y + y;
}
3.1 方法内联
- 方法内联是编译器最重要的优化手段
- 他可以消除方法调用的成本,也可以为其他优化手段建立基础。
public static void foo(Object object){
if(object != null){
System.out.println("do something");
}
}
public static void testInline(String[] args){
Object obj = null;
foo(obj);
}
- 例子中的testInline里面的代码都是无用代码,但是如果不做内联的话,就无法消除这些无用代码。
为什么说方法内联的过程不容易?
- 因为除了非虚方法以外,虚方法调用都必须在运行时进行方法接收者的多态选择,它们可能存在多余一个版本的方法接收者。Java中默认的实例方法都是虚方法
- 对于虚方法,编译器静态地去做内联的时候很难确定使用哪个版本
3.2 逃逸分析
- 基本原理:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中。(方法逃逸)。甚至还有可能被外部线程访问到,例如赋值给可以在其他线程中访问的实例变量。(线程逃逸)
- 如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度较低,则能为这个对象实例采取不同程度的优化。(例如可以将这个对象从堆上分配,改成在栈上分配,对象所占用的内存空间就会栈帧出栈而销毁。)
3.3 公共子表达式消除
- 如果一个表达式E已经被计算过了,并且从先前计算到现在E中所有变量没有发生变化,那么E这次出现成为公共子表达式
- 可以直接用前面计算过的表达式结果代替E。