新聞中心
Java 是一種跨平臺(tái)的編程語(yǔ)言。程序源代碼會(huì)被編譯為 字節(jié)碼bytecode,然后字節(jié)碼在運(yùn)行時(shí)被轉(zhuǎn)換為 機(jī)器碼machine code。解釋器interpreter 在物理機(jī)器上模擬出的抽象計(jì)算機(jī)上執(zhí)行字節(jié)碼指令。即時(shí)just-in-time(JIT)編譯發(fā)生在運(yùn)行期,而 預(yù)先ahead-of-time(AOT)編譯發(fā)生在構(gòu)建期。

本文將說(shuō)明解釋器、JIT 和 AOT 分別何時(shí)起作用,以及如何在 JIT 和 AOT 之間權(quán)衡。
源代碼、字節(jié)碼、機(jī)器碼
應(yīng)用程序通常是由 C、C++ 或 Java 等編程語(yǔ)言編寫(xiě)。用這些高級(jí)編程語(yǔ)言編寫(xiě)的指令集合稱(chēng)為源代碼。源代碼是人類(lèi)可讀的。要在目標(biāo)機(jī)器上執(zhí)行它,需要將源代碼轉(zhuǎn)換為機(jī)器可讀的機(jī)器碼。這個(gè)轉(zhuǎn)換工作通常是由 編譯器compiler
然而,在 Java 中,源代碼首先被轉(zhuǎn)換為一種中間形式,稱(chēng)為字節(jié)碼。字節(jié)碼是平臺(tái)無(wú)關(guān)的,所以 Java 被稱(chēng)為平臺(tái)無(wú)關(guān)編程語(yǔ)言。Java 編譯器 javac 將源代碼轉(zhuǎn)換為字節(jié)碼。然后解釋器解釋執(zhí)行字節(jié)碼。
下面是一個(gè)簡(jiǎn)單的 Java 程序, Hello.java:
//Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Inside Hello World!");
}
}使用 javac 編譯它,生成包含字節(jié)碼的 Hello.class 文件。
$ javac Hello.java
$ ls
Hello.class Hello.java現(xiàn)在,使用 javap 來(lái)反匯編 Hello.class 文件的內(nèi)容。使用 javap 時(shí)如果不指定任何選項(xiàng),它將打印基本信息,包括編譯這個(gè) .class 文件的源文件、包名稱(chēng)、公共和受保護(hù)字段以及類(lèi)的方法。
$ javap Hello.class
Compiled from "Hello.java"
public class Hello {
public Hello();
public static void main(java.lang.String[]);
}要查看 .class 文件中的字節(jié)碼內(nèi)容,使用 -c 選項(xiàng):
$ 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
} 要獲取更詳細(xì)的信息,使用 -v 選項(xiàng):
$ javap -v Hello.class解釋器,JIT 和 AOT
解釋器負(fù)責(zé)在物理機(jī)器上模擬出的抽象計(jì)算機(jī)上執(zhí)行字節(jié)碼指令。當(dāng)使用 javac 編譯源代碼,然后使用 java 執(zhí)行時(shí),解釋器在程序運(yùn)行時(shí)運(yùn)行并完成它的目標(biāo)。
$ javac Hello.java
$ java Hello
Inside Hello World!JIT 編譯器也在運(yùn)行期發(fā)揮作用。當(dāng)解釋器解釋 Java 程序時(shí),另一個(gè)稱(chēng)為運(yùn)行時(shí) 分析器profiler 的組件將靜默地監(jiān)視程序的執(zhí)行,統(tǒng)計(jì)各部分代碼被解釋的次數(shù)?;谶@些統(tǒng)計(jì)信息可以檢測(cè)出程序的 熱點(diǎn)hotspot,即那些經(jīng)常被解釋的代碼。一旦代碼被解釋次數(shù)超過(guò)設(shè)定的閾值,它們滿(mǎn)足被 JIT 編譯器直接轉(zhuǎn)換為機(jī)器碼的條件。所以 JIT 編譯器也被稱(chēng)為分析優(yōu)化的編譯器。從字節(jié)碼到機(jī)器碼的轉(zhuǎn)換是在程序運(yùn)行過(guò)程中進(jìn)行的,因此稱(chēng)為即時(shí)編譯。JIT 減少了解釋器將同一組指令模擬為機(jī)器碼的負(fù)擔(dān)。
AOT 編譯器在構(gòu)建期編譯代碼。在構(gòu)建時(shí)將需要頻繁解釋和 JIT 編譯的代碼直接編譯為機(jī)器碼可以縮短 Java 虛擬機(jī)Java Virtual Machine(JVM) 的預(yù)熱warm-up時(shí)間。(LCTT 譯注:Java 程序啟動(dòng)后首先字節(jié)碼被解釋執(zhí)行,此時(shí)執(zhí)行效率較低。等到程序運(yùn)行了足夠的時(shí)間后,代碼熱點(diǎn)被檢測(cè)出來(lái),JIT 開(kāi)始發(fā)揮作用,程序運(yùn)行效率提升。JIT 發(fā)揮作用之前的過(guò)程就是預(yù)熱。)AOT 是在 Java 9 中引入的一個(gè)實(shí)驗(yàn)性特性。jaotc 使用 Graal 編譯器(它本身也是用 Java 編寫(xiě)的)來(lái)實(shí)現(xiàn) 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!解釋和編譯發(fā)生的時(shí)機(jī)
下面通過(guò)例子來(lái)展示 Java 在什么時(shí)候使用解釋器,以及 JIT 和 AOT 何時(shí)參與進(jìn)來(lái)。這里有一個(gè)簡(jiǎn)單的程序 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("--------------------------------");
}
}
}在這個(gè)程序的 main() 方法中創(chuàng)建了一個(gè) Demo 對(duì)象的實(shí)例,并調(diào)用該實(shí)例的 square()方法,然后顯示 for 循環(huán)迭代變量的平方值。編譯并運(yùn)行它:
$ 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
--------------------------------上面的結(jié)果是由誰(shuí)產(chǎn)生的呢?是解釋器,JIT 還是 AOT?在目前的情況下,它完全是通過(guò)解釋產(chǎn)生的。我是怎么得出這個(gè)結(jié)論的呢?只有代碼被解釋的次數(shù)必須超過(guò)某個(gè)閾值時(shí),這些熱點(diǎn)代碼片段才會(huì)被加入 JIT 編譯隊(duì)列。只有這時(shí),JIT 編譯才會(huì)發(fā)揮作用。使用以下命令查看 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 編譯的條件。這個(gè)閾值是否可以手動(dòng)調(diào)整呢?是否有 JVM 標(biāo)志可以指示出方法是否被 JIT 編譯了呢?答案是肯定的,而且有多種方式可以達(dá)到這個(gè)目的。
使用 -XX:+PrintCompilation 選項(xiàng)可以查看一個(gè)方法是否被 JIT 編譯。除此之外,使用 -Xbatch 標(biāo)志可以提高輸出的可讀性。如果解釋和 JIT 同時(shí)發(fā)生,-Xbatch 可以幫助區(qū)分兩者的輸出。使用這些標(biāo)志如下:
$ 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
-------------------------------- 注意,上面命令的實(shí)際輸出太長(zhǎng)了,這里我只是截取了一部分。輸出很長(zhǎng)的原因是除了 Demo 程序的代碼外,JDK 內(nèi)部類(lèi)的函數(shù)也被編譯了。由于我的重點(diǎn)是 Demo.java 代碼,我希望排除內(nèi)部包的函數(shù)來(lái)簡(jiǎn)化輸出。通過(guò)選項(xiàng) -XX:CompileCommandFile 可以禁用內(nèi)部類(lèi)的 JIT:
$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Demo在選項(xiàng) -XX:CompileCommandFile 指定的文件 hotspot_compiler 中包含了要排除的包:
$ cat hotspot_compiler
quiet
exclude java/* *
exclude jdk/* *
exclude sun/* *第一行的 quiet 告訴 JVM 不要輸出任何關(guān)于被排除類(lèi)的內(nèi)容。用 -XX:CompileThreshold 將 JIT 閾值設(shè)置為 5。這意味著在解釋 5 次之后,就會(huì)進(jìn)行 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
--------------------------------好像輸出結(jié)果跟只用解釋時(shí)并沒(méi)有什么區(qū)別。根據(jù) Oracle 的文檔,這是因?yàn)橹挥薪?nbsp;TieredCompilation 時(shí) -XX:CompileThreshold 才會(huì)生效:
$ 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() 方法一起,構(gòu)造方法也被 JIT 編譯了。在 for 循環(huán)中調(diào)用 square() 之前要先構(gòu)造 Demo 實(shí)例,所以構(gòu)造方法的解釋次數(shù)同樣達(dá)到 JIT 編譯閾值。這個(gè)例子說(shuō)明了在解釋發(fā)生之后何時(shí) JIT 會(huì)介入。
要查看編譯后的代碼,需要使用 -XX:+PrintAssembly 標(biāo)志,該標(biāo)志僅在庫(kù)路徑中有反匯編器時(shí)才起作用。對(duì)于 OpenJDK,使用 hsdis 作為反匯編器。下載合適版本的反匯編程序庫(kù),在本例中是 hsdis-amd64.so,并將其放在 Java_HOME/lib/server 目錄下。使用時(shí)還需要在 -XX:+PrintAssembly 之前增加 -XX:+UnlockDiagnosticVMOptions 選項(xiàng)。否則,JVM 會(huì)給你一個(gè)警告。
完整命令如下:
$ 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
----------------------------------------------------------------------
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 相關(guān)的部分。
現(xiàn)在再來(lái)看看 AOT 編譯。它是在 JDK9 中引入的特性。AOT 是用于生成 .so 這樣的庫(kù)文件的靜態(tài)編譯器。用 AOT 可以將指定的類(lèi)編譯成 .so 庫(kù)。這個(gè)庫(kù)可以直接執(zhí)行,而不用解釋或 JIT 編譯。如果 JVM 沒(méi)有檢測(cè)到 AOT 編譯的代碼,它會(huì)進(jìn)行常規(guī)的解釋和 JIT 編譯。
使用 AOT 編譯的命令如下:
$ jaotc --output=libDemo.so Demo.class用下面的命令來(lái)查看共享庫(kù)的符號(hào)表:
$ nm libDemo.so要使用生成的 .so 庫(kù),使用 -XX:+UnlockExperimentalVMOptions 和 -XX:AOTLibrary:
$ 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從輸出上看,跟完全用解釋的情況沒(méi)有區(qū)別。為了確認(rèn) AOT 發(fā)揮了作用,使用 -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.()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 要確認(rèn)沒(méi)有發(fā)生 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.()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 庫(kù)文件。否則,過(guò)時(shí)的的 AOT 編譯庫(kù)文件不會(huì)起作用。例如,修改 square() 方法,使其計(jì)算立方值:
//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但不重新生成 libDemo.so。使用下面命令運(yùn)行 Demo:
$ 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:: (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 檢測(cè)出它已經(jīng)過(guò)時(shí)了。每次生成 .class 文件時(shí),都會(huì)在類(lèi)文件中添加一個(gè)指紋,并在 AOT 庫(kù)中保存該指紋。修改源代碼后類(lèi)指紋與舊的 AOT 庫(kù)中的指紋不匹配了,所以沒(méi)有執(zhí)行 AOT 編譯生成的原生機(jī)器碼。從輸出可以看出,現(xiàn)在實(shí)際上是 JIT 在起作用(注意 -XX:CompileThreshold 被設(shè)置為了 3)。
AOT 和 JIT 之間的權(quán)衡
如果你的目標(biāo)是減少 JVM 的預(yù)熱時(shí)間,請(qǐng)使用 AOT,這可以減少運(yùn)行時(shí)負(fù)擔(dān)。問(wèn)題是 AOT 沒(méi)有足夠的數(shù)據(jù)來(lái)決定哪段代碼需要預(yù)編譯為原生代碼。相比之下,JIT 在運(yùn)行時(shí)起作用,卻對(duì)預(yù)熱時(shí)間有一定的影響。然而,它將有足夠的分析數(shù)據(jù)來(lái)更高效地編譯和反編譯代碼。
分享題目:JVM解釋和編譯指南
網(wǎng)頁(yè)URL:http://www.dlmjj.cn/article/coigcsc.html


咨詢(xún)
建站咨詢(xún)
