深入理解JVM中的即时编译器(JIT)
前言:原始Class字节码通过JVM 解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。这就是传统的JVM的解释器(Interpreter)的功能。为了解决这种效率问题,引入了 JIT(即时编译) 技术
关于java代码如何被解析为操作指令
推荐参考:Java代码的编译与执行过程
1、JIT编译器概述
JVM执行Java代码的过程是把Java源文件编译成字节码,然后通过JIT编译器将字节码转换成本地机器代码
。不同于传统编译器事先编译,JIT编译器在运行时编译
,将那些经常运行的代码块(热点代码)编译成与平台相关的机器语言,从而提高程序的执行效率
。
2、工作方式
JVM启动时,并不会立即将所有字节码编译成机器码,而是首先解释执行字节码。当某部分代码被频繁执行时,被识别为“热点代码”
。JVM就会使用JIT编译器将这部分字节码编译成对应平台的本地机器码
,以提高执行效率。这使得Java程序能够以接近本地应用程序的速度运行,同时保持了跨平台的特性。
3、JIT编译器优化技术
(1)常见优化方式
JIT编译器采用了多种高级优化技术来提高程序运行的性能,包括但不限于:
- 方法内联 - 将一个方法的内容直接嵌入到调用它的地方,以减少方法调用的开销。
- 循环优化 - 改进循环的执行效率,比如通过循环展开减少循环次数。
- 死代码消除 - 移除不会执行到的代码。
- 逃逸分析 - 分析对象的作用域,决定是否可以堆优化,比如栈分配。
- 公共子表达式消除 - 查找并删除代码中重复计算的子表达式。
(2)举例说明
- 方法内联:假设有如下的代码:
int add(int a, int b) {
return a + b;
}
int calculate(int x, int y) {
int result = add(x, y);
return result * 2;
}
JIT 编译器可以将 add 方法的内容内联到 calculate 方法中,使得调用 add 方法的开销减少,从而提高性能。
- 循环优化:考虑以下简单的循环:
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
JIT 编译器可以对循环进行优化,比如循环展开:
int sum = 0;
sum += 0;
sum += 1;
sum += 2;
// ... 省略后续的加法运算
这样可以减少循环的次数,从而提高效率。
- 死代码消除:考虑以下代码:
int result = 0;
if (false) {
result = 100; // 死代码,永远不会执行
}
JIT 编译器可以检测到 if (false) 这一条件永远不会成立,因此可以移除 result = 100; 这行代码。
- 逃逸分析:当一个对象的作用域被分析后,JIT 编译器可以决定是否可以在栈上分配该对象,而不是在堆上进行分配。
- 公共子表达式消除:考虑以下代码:
int result1 = a * b + c;
int result2 = a * b + c;
JIT 编译器可以发现 a * b + c 这个子表达式在两处都被计算,因此可以将其优化为一个单独的计算,从而减少重复计算的开销。
4、JIT的类型
在HotSpot虚拟机中,有两种类型的JIT编译器:
- Client Compiler(C1) - 针对客户端应用程序,优化启动时间,以较少的编译优化来实现更快的编译速度。
- Server Compiler(C2) - 针对服务端应用程序,进行更多的优化来提高峰值性能。
在JDK 8及之后版本中,还引入了Graal编译器,它是一个基于Java的JIT编译器,它可以作为C2编译器的替代品。
5、JIT面临挑战
尽管JIT编译器大大提高了程序的执行效率,但它也面临一些挑战,比如:
- 编译延时 - JIT编译器必须在程序运行时进行编译,这会增加一些延时。
- 资源消耗 - 编译过程中消耗CPU和内存资源。
- 调试和分析 - JIT编译后的代码难以调试和性能分析。
6、总结
JIT编译器在运行时
编译字节码为本地机器码
,从而提高程序的性能。
JIT采用了多种优化技术:方法内联、逃逸分析、循环优化、死代码消除 等。