JVM 解释和编译指南

通过理解解释、即时编译和预先编译之间的区别,有效地使用它们。

JVM 解释和编译指南

通过理解解释、即时编译和预先编译之间的区别,有效地使用它们。

Java 是一种跨平台的编程语言。程序源代码会被编译为 字节码 bytecode ,然后字节码在运行时被转换为 机器码 machine code 解释器 interpreter 在物理机器上模拟出的抽象计算机上执行字节码指令。 即时 just-in-time (JIT)编译发生在运行期,而 预先 ahead-of-time (AOT)编译发生在构建期。

本文将说明解释器、JIT 和 AOT 分别何时起作用,以及如何在 JIT 和 AOT 之间权衡。

源代码、字节码、机器码

应用程序通常是由 C、C++ 或 Java 等编程语言编写。用这些高级编程语言编写的指令集合称为源代码。源代码是人类可读的。要在目标机器上执行它,需要将源代码转换为机器可读的机器码。这个转换工作通常是由 编译器 compiler 来完成的。

然而,在 Java 中,源代码首先被转换为一种中间形式,称为字节码。字节码是平台无关的,所以 Java 被称为平台无关编程语言。Java 编译器 javac 将源代码转换为字节码。然后解释器解释执行字节码。

下面是一个简单的 Java 程序, Hello.java

“`
//Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println(“Inside Hello World!”);
}
}

“`

使用 javac 编译它,生成包含字节码的 Hello.class 文件。

“`
$ javac Hello.java
$ ls
Hello.class Hello.java

“`

现在,使用 javap 来反汇编 Hello.class 文件的内容。使用 javap 时如果不指定任何选项,它将打印基本信息,包括编译这个 .class 文件的源文件、包名称、公共和受保护字段以及类的方法。

“`
$ javap Hello.class
Compiled from “Hello.java”
public class Hello {
public Hello();
public static void main(java.lang.String[]);
}

“`

要查看 .class 文件中的字节码内容,使用 -c 选项:

“`
$ javap -c Hello.class
Compiled from “Hello.java”
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”“:()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Inside Hello World!
5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

“`

要获取更详细的信息,使用 -v 选项:

“`
$ javap -v Hello.class

“`

解释器,JIT 和 AOT

解释器负责在物理机器上模拟出的抽象计算机上执行字节码指令。当使用 javac 编译源代码,然后使用 java 执行时,解释器在程序运行时运行并完成它的目标。

“`
$ javac Hello.java
$ java Hello
Inside Hello World!

“`

JIT 编译器也在运行期发挥作用。当解释器解释 Java 程序时,另一个称为运行时 分析器 profiler 的组件将静默地监视程序的执行,统计各部分代码被解释的次数。基于这些统计信息可以检测出程序的 热点 hotspot ,即那些经常被解释的代码。一旦代码被解释次数超过设定的阈值,它们满足被 JIT 编译器直接转换为机器码的条件。所以 JIT 编译器也被称为分析优化的编译器。从字节码到机器码的转换是在程序运行过程中进行的,因此称为即时编译。JIT 减少了解释器将同一组指令模拟为机器码的负担。

AOT 编译器在构建期编译代码。在构建时将需要频繁解释和 JIT 编译的代码直接编译为机器码可以缩短 Java 虚拟机 Java Virtual Machine (JVM) 的 预热 warm-up 时间。(LCTT 译注:Java 程序启动后首先字节码被解释执行,此时执行效率较低。等到程序运行了足够的时间后,代码热点被检测出来,JIT 开始发挥作用,程序运行效率提升。JIT 发挥作用之前的过程就是预热。)AOT 是在 Java 9 中引入的一个实验性特性。jaotc 使用 Graal 编译器(它本身也是用 Java 编写的)来实现 AOT 编译。

Hello.java 为例:

“`
//Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println(“Inside Hello World!”);
}
}

$ javac Hello.java
$ jaotc –output libHello.so Hello.class
$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libHello.so Hello
Inside Hello World!

“`

解释和编译发生的时机

下面通过例子来展示 Java 在什么时候使用解释器,以及 JIT 和 AOT 何时参与进来。这里有一个简单的程序 Demo.java :

“`
//Demo.java
public class Demo {
public int square(int i) throws Exception {
return(i*i);
}

public static void main(String[] args) throws Exception {
    for (int i = 1; i <= 10; i++) {
        System.out.println("call " + Integer.valueOf(i));
        long a = System.nanoTime();
        Int r = new Demo().square(i);
        System.out.println("Square(i) = " + r);
        long b = System.nanoTime();
        System.out.println("elapsed= " + (b-a));
        System.out.println("--");
    }
}

}

“`

在这个程序的 main() 方法中创建了一个 Demo 对象的实例,并调用该实例的 square()方法,然后显示 for 循环迭代变量的平方值。编译并运行它:

“`
$ javac Demo.java
$ java Demo
1 iteration
Square(i) = 1

Time taken= 8432439

2 iteration
Square(i) = 4

Time taken= 54631

.
.

.

10 iteration
Square(i) = 100

Time taken= 66498

“`

上面的结果是由谁产生的呢?是解释器,JIT 还是 AOT?在目前的情况下,它完全是通过解释产生的。我是怎么得出这个结论的呢?只有代码被解释的次数必须超过某个阈值时,这些热点代码片段才会被加入 JIT 编译队列。只有这时,JIT 编译才会发挥作用。使用以下命令查看 JDK 11 中的该阈值:

“`
$ java -XX:+PrintFlagsFinal -version | grep CompileThreshold
intx CompileThreshold = 10000 {pd product} {default}
[…]
openjdk version “11.0.13” 2021-10-19
OpenJDK Runtime Environment 18.9 (build 11.0.13+8)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8, mixed mode, sharing)

“`

上面的输出表明,一段代码被解释 10,000 次才符合 JIT 编译的条件。这个阈值是否可以手动调整呢?是否有 JVM 标志可以指示出方法是否被 JIT 编译了呢?答案是肯定的,而且有多种方式可以达到这个目的。

使用 -XX:+PrintCompilation 选项可以查看一个方法是否被 JIT 编译。除此之外,使用 -Xbatch 标志可以提高输出的可读性。如果解释和 JIT 同时发生,-Xbatch 可以帮助区分两者的输出。使用这些标志如下:

“`
$ java -Xbatch -XX:+PrintCompilation Demo
34 1 b 3 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
35 2 n 0 jdk.internal.misc.Unsafe::getObjectVolatile (native)
35 3 b 3 java.lang.Object:: (1 bytes)
[…]
210 269 n 0 java.lang.reflect.Array::newArray (native) (static)
211 270 b 3 java.lang.String::substring (58 bytes)

[…]

10 iteration
Square(i) = 100

Time taken= 50150

“`

注意,上面命令的实际输出太长了,这里我只是截取了一部分。输出很长的原因是除了 Demo 程序的代码外,JDK 内部类的函数也被编译了。由于我的重点是 Demo.java 代码,我希望排除内部包的函数来简化输出。通过选项 -XX:CompileCommandFile 可以禁用内部类的 JIT:

“`
$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Demo

“`

在选项 -XX:CompileCommandFile 指定的文件 hotspot_compiler 中包含了要排除的包:

“`
$ cat hotspot_compiler
quiet
exclude java/* *
exclude jdk/* *
exclude sun/* *

“`

第一行的 quiet 告诉 JVM 不要输出任何关于被排除类的内容。用 -XX:CompileThreshold 将 JIT 阈值设置为 5。这意味着在解释 5 次之后,就会进行 JIT 编译:

“`
$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
-XX:CompileThreshold=5 Demo
47 1 n 0 java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)
(static)
47 2 n 0 java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)
47 3 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LLLLLLL)L (native)
(static)
48 4 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)I (native) (static)
48 5 n 0 java.lang.invoke.MethodHandle::invokeBasic()I (native)
48 6 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LL)I (native)
(static)
[…]
1 iteration
69 40 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)
(static)
[…]
Square(i) = 1
78 48 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native)
(static)
79 49 n 0 java.lang.invoke.MethodHandle::invokeBasic(ILIJ)I (native)
[…]
86 54 n 0 java.lang.invoke.MethodHandle::invokeBasic(J)L (native)
87 55 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)
(static)

Time taken= 8962738

2 iteration
Square(i) = 4

Time taken= 26759

10 iteration
Square(i) = 100

Time taken= 26492

“`

好像输出结果跟只用解释时并没有什么区别。根据 Oracle 的文档,这是因为只有禁用 TieredCompilation-XX:CompileThreshold 才会生效:

“`
$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
-XX:-TieredCompilation -XX:CompileThreshold=5 Demo
124 1 n java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static)
127 2 n java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)
[…]
1 iteration
187 40 n java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native) (static)
[…]
(native) (static)
212 54 n java.lang.invoke.MethodHandle::invokeBasic(J)L (native)
212 55 n java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native) (static)
Time taken= 12337415

[…]

4 iteration
Square(i) = 16

Time taken= 37183

5 iteration
214 56 b Demo:: (5 bytes)
215 57 b Demo::square (16 bytes)
Square(i) = 25

Time taken= 983002

6 iteration
Square(i) = 36
Time taken= 81589
[…]
10 iteration
Square(i) = 100
Time taken= 52393

“`

可以看到在第五次迭代之后,代码片段被 JIT 编译了:

“`

5 iteration
214 56 b Demo:: (5 bytes)
215 57 b Demo::square (16 bytes)
Square(i) = 25

Time taken= 983002

“`

可以看到,与 square() 方法一起,构造方法也被 JIT 编译了。在 for 循环中调用 square() 之前要先构造 Demo 实例,所以构造方法的解释次数同样达到 JIT 编译阈值。这个例子说明了在解释发生之后何时 JIT 会介入。

要查看编译后的代码,需要使用 -XX:+PrintAssembly 标志,该标志仅在库路径中有反汇编器时才起作用。对于 OpenJDK,使用 hsdis 作为反汇编器。下载合适版本的反汇编程序库,在本例中是 hsdis-amd64.so,并将其放在 Java_HOME/lib/server 目录下。使用时还需要在 -XX:+PrintAssembly 之前增加 -XX:+UnlockDiagnosticVMOptions 选项。否则,JVM 会给你一个警告。

完整命令如下:

“`
$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \ -XX:-TieredCompilation -XX:CompileThreshold=5 -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly Demo
[…]
5 iteration
178 56 b Demo:: (5 bytes)
Compiled method (c2) 178 56 Demo:: (5 bytes)
total in heap [0x00007fd4d08dad10,0x00007fd4d08dafe0] = 720
relocation [0x00007fd4d08dae88,0x00007fd4d08daea0] = 24
[…]
handler table [0x00007fd4d08dafc8,0x00007fd4d08dafe0] = 24
[…]
dependencies [0x00007fd4d08db3c0,0x00007fd4d08db3c8] = 8
handler table [0x00007fd4d08db3c8,0x00007fd4d08db3f8] = 48

via: https://opensource.com/article/22/8/interpret-compile-java

作者:Jayashree Huttanagoudar 选题:lkxed 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

主题测试文章,只做测试使用。发布者:eason,转转请注明出处:https://aicodev.cn/2023/11/06/jvm-%e8%a7%a3%e9%87%8a%e5%92%8c%e7%bc%96%e8%af%91%e6%8c%87%e5%8d%97/

Like (0)
eason的头像eason
Previous 2023年11月6日
Next 2023年11月6日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信