Java 是一种平台独立的语言。程序在编译后会被转换为字节码。此字节码在运行时会被转换为机器码。解释器模拟在特定物理机器上执行抽象机器的字节码指令。即时 (JIT) 编译发生在执行期间的某个时刻,而提前 (AOT) 编译发生在构建时。
本文解释了何时使用解释器,以及何时发生 JIT 和 AOT。我还讨论了 JIT 和 AOT 之间的权衡。
源代码、字节码、机器码
应用程序通常使用诸如 C、C++ 或 Java 之类的编程语言编写。使用高级编程语言编写的指令集称为源代码。源代码是人类可读的。为了在目标机器上执行它,源代码需要转换为机器码,即机器可读的代码。源代码通常由编译器转换为机器码。
然而,在 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
的输出取决于使用的选项。 如果您不选择任何选项,它会打印基本信息,包括此类文件是从哪个源文件编译的、包名称、公共和受保护的字段以及类的方法。
$ 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."<init>":()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 程序时,另一个称为运行时分析器的组件会静默地监视程序的执行,以观察代码的哪个部分正在被解释以及多少次。 这些统计信息有助于检测程序的热点,即那些经常被解释的代码部分。 一旦它们被解释超过设定的阈值,它们就有资格被 JIT 编译器直接转换为机器码。 JIT 编译器也称为profile-guided编译器。 字节码到本机代码的转换是动态发生的,因此得名即时。 JIT 减少了解释器将同一组指令模拟为机器码的开销。
AOT 编译器在构建时编译代码。 在构建时生成经常解释和 JIT 编译的代码可以缩短 Java 虚拟机 (JVM) 的预热时间。 此编译器在 Java 9 中作为实验性功能引入。 jaotc
工具使用 Graal 编译器(它本身是用 Java 编写的)进行 AOT 编译。
这是一个 Hello 程序的示例用例
//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 何时介入。 考虑一个简单的 Java 程序,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 编译? 是的,有多种选项可以实现此目的。
学习某个方法是否经过 JIT 编译的一个选项是 -XX:+PrintCompilation
。 与此选项一起,标志 -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::<init> (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 不要写入任何关于排除类的信息。 要调整 JIT 阈值,请使用 -XX:CompileThreshold
,并将该值设置为 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::<init> (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::<init> (5 bytes)
215 57 b Demo::square (16 bytes)
Square(i) = 25
Time taken= 983002
--------------------------------
除了 square()
方法之外,构造函数也经过 JIT 编译,因为在调用 square()
之前,for
循环内有一个 Demo 实例。 因此,它也将达到阈值并进行 JIT 编译。 此示例说明了 JIT 何时在解释后发挥作用。
要查看代码的编译版本,请使用 -XX:+PrintAssembly flag
,它仅在库路径中存在反汇编程序时才有效。 对于 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::<init> (5 bytes)
Compiled method (c2) 178 56 Demo::<init> (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
----------------------------------------------------------------------
Demo.square(I)I [0x00007fd4d08db1c0, 0x00007fd4d08db2b8] 248 bytes
[Entry Point]
[Constants]
# {method} {0x00007fd4b841f4b0} 'square' '(I)I' in 'Demo'
# this: rsi:rsi = 'Demo'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
[...]
[Stub Code]
0x00007fd4d08db280: movabs $0x0,%rbx ; {no_reloc}
0x00007fd4d08db28a: jmpq 0x00007fd4d08db28a ; {runtime_call}
0x00007fd4d08db28f: movabs $0x0,%rbx ; {static_stub}
0x00007fd4d08db299: jmpq 0x00007fd4d08db299 ; {runtime_call}
[Exception Handler]
0x00007fd4d08db29e: jmpq 0x00007fd4d08bb880 ; {runtime_call ExceptionBlob}
[Deopt Handler Code]
0x00007fd4d08db2a3: callq 0x00007fd4d08db2a8
0x00007fd4d08db2a8: subq $0x5,(%rsp)
0x00007fd4d08db2ad: jmpq 0x00007fd4d08a01a0 ; {runtime_call DeoptimizationBlob}
0x00007fd4d08db2b2: hlt
0x00007fd4d08db2b3: hlt
0x00007fd4d08db2b4: hlt
0x00007fd4d08db2b5: hlt
0x00007fd4d08db2b6: hlt
0x00007fd4d08db2b7: hlt
ImmutableOopMap{rbp=NarrowOop }pc offsets: 96
ImmutableOopMap{}pc offsets: 112
ImmutableOopMap{rbp=Oop }pc offsets: 148 Square(i) = 25
Time taken= 2567698
--------------------------------
6 iteration
Square(i) = 36
Time taken= 76752
[...]
--------------------------------
10 iteration
Square(i) = 100
Time taken= 52888
输出很长,所以我只包含了与 Demo.java
相关的输出。
现在是 AOT 编译的时候了。 此选项已在 JDK9 中引入。 AOT 是一个静态编译器,用于生成 .so
库。 使用 AOT,可以将感兴趣的类编译为创建一个 .so
库,该库可以直接执行,而不是解释或 JIT 编译。 如果 JVM 没有找到任何 AOT 编译的代码,则会进行通常的解释和 JIT 编译。
用于 AOT 编译的命令如下
$ jaotc --output=libDemo.so Demo.class
要查看共享库中的符号,请使用以下命令
$ nm libDemo.so
要使用生成的 .so
库,请将 -XX:AOTLibrary
与 -XX:+UnlockExperimentalVMOptions
一起使用,如下所示
$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so Demo
1 iteration
Square(i) = 1
Time taken= 7831139
--------------------------------
2 iteration
Square(i) = 4
Time taken= 36619
[...]
10 iteration
Square(i) = 100
Time taken= 42085
此输出看起来好像是解释版本本身。 为了确保使用了 AOT 编译的代码,请使用 -XX:+PrintAOT
$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
28 1 loaded ./libDemo.so aot library
80 1 aot[ 1] Demo.main([Ljava/lang/String;)V
80 2 aot[ 1] Demo.square(I)I
80 3 aot[ 1] Demo.<init>()V
1 iteration
Square(i) = 1
Time taken= 7252921
--------------------------------
2 iteration
Square(i) = 4
Time taken= 57443
[...]
10 iteration
Square(i) = 100
Time taken= 53586
只是为了确保没有发生 JIT 编译,请使用以下命令
$ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation \ -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation \ -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
19 1 loaded ./libDemo.so aot library
77 1 aot[ 1] Demo.square(I)I
77 2 aot[ 1] Demo.main([Ljava/lang/String;)V
77 3 aot[ 1] Demo.<init>()V
77 2 aot[ 1] Demo.main([Ljava/lang/String;)V made not entrant
[...]
4 iteration
Square(i) = 16
Time taken= 43366
[...]
10 iteration
Square(i) = 100
Time taken= 59554
如果对进行 AOT 的源代码进行了任何小的更改,务必确保再次创建相应的 .so
。 否则,过时的 AOT 编译的 .so
不会有任何效果。 例如,对 square 函数进行一个小更改,使其现在计算立方体
//Demo.java
public class Demo {
public int square(int i) throws Exception {
return(i*i*i);
}
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 10; i++) {
System.out.println("" + Integer.valueOf(i)+" iteration");
long start = System.nanoTime();
int r= new Demo().square(i);
System.out.println("Square(i) = " + r);
long end = System.nanoTime();
System.out.println("Time taken= " + (end-start));
System.out.println("--------------------------------");
}
}
}
现在,再次编译 Demo.java
$ java Demo.java
但是,不要使用 jaotc
创建 libDemo.so
。 而是使用此命令
$ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
20 1 loaded ./libDemo.so aot library
74 1 n java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static)
2 iteration
sqrt(i) = 8
Time taken= 43838
--------------------------------
3 iteration
137 56 b Demo::<init> (5 bytes)
138 57 b Demo::square (6 bytes)
sqrt(i) = 27
Time taken= 534649
--------------------------------
4 iteration
sqrt(i) = 64
Time taken= 51916
[...]
10 iteration
sqrt(i) = 1000
Time taken= 47132
虽然加载了旧版本的 libDemo.so
,但 JVM 将其检测为过时的。 每次创建 .class
文件时,都会有一个指纹进入类文件,并且类指纹会保留在 AOT 库中。 由于类指纹与 AOT 库中的指纹不同,因此不会使用 AOT 编译的本机代码。 相反,该方法现在经过 JIT 编译,因为 -XX:CompileThreshold
设置为 3。
AOT 还是 JIT?
如果您的目标是减少 JVM 的预热时间,请使用 AOT,这可以减轻运行时的负担。 问题在于 AOT 没有足够的数据来决定需要预编译哪些代码到本机代码。 相比之下,JIT 在运行时介入并影响预热时间。 但是,它将有足够的分析数据来更有效地编译和反编译代码。
评论已关闭。