JIT的优化
JIT的优化
众所周知,JAVA 为了实现跨平台,增加了一层 JVM,不同平台的 JVM 负责解释执行字节码文件。虽然有一层解释会影响效率,但好处是跨平台,字节码文件是平台无关的。
在 JAVA 1.2 之后,增加了即时编译(Just-in-Time Compilation,简称 JIT) 的机制,在运行时可以将执行次数较多的热点代码编译为机器码,这样就不需要 JVM 再解释一遍了,可以直接执行,增加运行效率。
表达式提升(expression hoisting)
先来看个例子,在这个 hoisting
方法中,for 循环里每次都会定义一个变量 y
,然后通过将 x*y
的结果存储在一个 result 变量中,然后使用这个变量进行各种操作
public void hoisting(int x) {
for (int i = 0; i < 1000; i = i + 1) {
// 循环不变的计算
int y = 654;
int result = x * y;
// ...... 基于这个 result 变量的各种操作
}
}
但是这个例子里,result 的结果是固定的,并不会跟着循环而更新。所以完全可以将 result 的计算提取到循环之外,这样就不用每次计算了。JIT 分析后会对这段代码进行优化,进行表达式提升的操作:
public void hoisting(int x) {
int y = 654;
int result = x * y;
for (int i = 0; i < 1000; i = i + 1) {
// ...... 基于这个 result 变量的各种操作
}
}
这样一来,result 不用每次计算了,而且也完全不影响执行结果,大大提升了执行效率。 注意,编译器更喜欢局部变量,而不是静态变量或者成员变量;因为静态变量是“逃逸在外的”,多个线程都可以访问到,而局部变量是线程私有的,不会被其他线程访问和修改。
编译器在处理静态变量/成员变量时,会比较保守,不会轻易优化。
表达式下沉(expression sinking)
和表达式提升类似的,还有个表达式下沉的优化,比如下面这段代码:
public void sinking(int i) {
int result = 543 * i;
if (i % 2 == 0) {
// 使用 result 值的一些逻辑代码
} else {
// 一些不使用 result 的值的逻辑代码
}
}
由于在 else 分支里,并没有使用 result 的值,可每次不管什么分支都会先计算 result,这就没必要了。JIT 会把 result 的计算表达式移动到 if 分支里,这样就避免了每次进入方法对 result 的计算,这个操作就叫表达式下沉:
public void sinking(int i) {
if (i % 2 == 0) {
int result = 543 * i;
// 使用 result 值的一些逻辑代码
} else {
// 一些不使用 result 的值的逻辑代码
}
}
循环展开(Loop unwinding/loop unrolling)
下面这个 for 循环,一共要循环 10w 次,每次都需要检查条件。
for (int i = 0; i < 100000; i++) {
delete(i);
}
在编译器的优化后,会删除一定的循环次数,从而降低索引递增和条件检查操作而引起的开销:
for (int i = 0; i < 20000; i+=5) {
delete(i);
delete(i + 1);
delete(i + 2);
delete(i + 3);
delete(i + 4);
}
除了循环展开,循环还有一些优化机制,比如循环剥离、循环交换、循环分裂、循环合并……
内联优化(Inling)
JVM 的方法调用是个栈的模型,每次方法调用都需要一个压栈(push)和出栈(pop)的操作,编译器也会对调用模型进行优化,将一些方法的调用进行内联。
内联就是抽取要调用的方法体代码,到当前方法中直接执行,这样就可以避免一次压栈出栈的操作,提升执行效率。比如下面这个方法:
public void inline(){
int a = 5;
int b = 10;
int c = calculate(a, b);
// 使用 c 处理……
}
public int calculate(int a, int b){
return a + b;
}
在编译器内联优化后,会将 calculate
的方法体抽取到 inline
方法中,直接执行,而不用进行方法调用:
public void inline(){
int a = 5;
int b = 10;
int c = a + b;
// 使用 c 处理……
}
不过这个内联优化是有一些限制的,比如 native 的方法就不能内联优化
提前置空
来先看一个例子,在这个例子中 was finalized!
会在 done.
之前输出,这个也是因为 JIT 的优化导致的。
class A {
// 对象被回收前,会触发 finalize
@Override protected void finalize() {
System.out.println(this + " was finalized!");
}
public static void main(String[] args) throws InterruptedException {
A a = new A();
System.out.println("Created " + a);
for (int i = 0; i < 1_000_000_000; i++) {
if (i % 1_000_00 == 0)
System.gc();
}
System.out.println("done.");
}
}
//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法输出
done.
从例子中可以看到,如果 a
在循环完成后已经不再使用了,则会出现先执行finalize的情况;虽然从对象作用域来说,方法没有执行完,栈帧并没有出栈,但是还是会被提前执行。
这就是因为 JIT 认为 a
对象在循环内和循环后都不会在使用,所以提前给它置空了,帮助 GC 回收;如果禁用 JIT,那就不会出现这个问题……
这个提前回收的机制,还是有点风险的,在某些场景下会引起 BUG,比如《一个JDK线程池BUG引发的GC机制思考》
HotSpot VM JIT 的各种优化项
上面只是介绍了几个简单常用的编译优化机制,JVM JIT 更多的优化机制可以参考下面这个图。这是 OpenJDK 文档中提供的一个 pdf 材料,里面列出了 HotSpot JVM 的各种优化机制,相当多……
如何避免因 JIT 导致的问题?
小伙伴:“JIT 这么多优化机制,很容易出问题啊,我平时写代码要怎么避开这些呢”
而那个提前置空导致的问题,出现的几率也很低,只要你规范写代码基本不会遇到的。 在日常编码过程中,不用刻意的猜测 JIT 的优化机制,JVM 也不会完整的告诉你所有的优化。而且这种东西不同版本效果不一样,就算搞明白了一个机制,可能到下个版本就完全不一样了。
所以,如果不是搞编译器开发的话,JIT 相关的编译知识,作为一个知识储备就好。
也不用去猜测 JIT 到底会怎么优化你的代码,你(可能)猜不准……
文章抄录自:https://segmentfault.com/a/1190000040097711