注意:按照JEP 295描述,目前版本的AOT,僅支持64位Linux操作系統。
jaotc使用
首先需要下載最新的Java 9(JDK),本文編寫時,最新版本是Build 152。下載好的JDK只需要解壓即可使用,特別注意使用前設置好PATH
和JAVA_HOME
兩個環境變量,避免和機器上已經安裝的JDK混淆。筆者安裝到了$HOME/bin/jdk-9,並設置了:
export PATH=~/bin/jdk-9/bin:$PATH export JAVA_HOME=~/bin/jdk-9
需要使用jaotc
,首先需要有個測試類,首先從Hello World開始:
class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
代碼非常簡單,但是在執行jaotc之前,還需要將其編譯成class文件,直接使用javac即可:
$ javac HelloWorld.java
執行成功之後,會生成HelloWorld.class文件。此時直接使用java命令,已經可以正常運行這個類:
$ java HelloWorld Hello World!
這時,就可以基於這個class文件,通過jaotc
命令將其編譯成二進制文件了。
$ jaotc --output libHelloWorld.so HelloWorld.class
如果一切正常,會生成libHelloWorld.so文件。
如果出現類似Exception in thread "main" java.lang.UnsatisfiedLinkError: /home/babydragon/bin/jdk-9/lib/libjelfshim.so: libelf.so.1: 無法打開共享對象文件: 沒有那個文件或目錄
的錯誤,是因為jaotc
需要依賴libelf動態鏈接庫來創建elf文件(最終生成的libHelloWorld.so文件是一個靜態鏈接的elf文件)。筆者使用的是Gentoo系統,需要安裝dev-libs/elfutils包,以提供libelf.so這個動態連接庫。安裝之後可以通過ldd
命令進行確認:
$ ldd $JAVA_HOME/lib/libjelfshim.so linux-vdso.so.1 (0x00007ffd001f3000) libelf.so.1 => /usr/lib64/libelf.so.1 (0x00007f25ea2ce000) libc.so.6 => /lib64/libc.so.6 (0x00007f25e9f35000) libz.so.1 => /lib64/libz.so.1 (0x00007f25e9d1d000) /lib64/ld-linux-x86-64.so.2 (0x0000562318d51000)
前面通過jaotc
命令成功生成了libHelloWorld.so。雖然命令裡面參照JEP 295的示例將生成的文件後綴設置成了so,但如果使用ldd
命令查看,會發現它其實是一個靜態鏈接庫:
$ ldd libHelloWorld.so statically linked
通過nm
命令,可以看見代碼段中的函數入口:
$ nm libHelloWorld.so 0000000000002420 t HelloWorld.()V 0000000000002520 t HelloWorld.main([Ljava/lang/String;)V
最後,需要執行時需要通過參數-XX:AOTLibrary
參數指定需要加載的經過aot預編譯好的共享庫文件:
java -XX:AOTLibrary=./libHelloWorld.so HelloWorld
注意:雖然已經將整個HelloWorld類都通過jaotc編譯成共享庫文件,運行時仍然需要依賴原有的HelloWorld.class文件。
此時執行的輸出,和之前不使用AOT的輸出完全相同。
來把大的——將java.base模塊編譯成AOT庫
JEP 295中已經說明,在Java 9初始發布的時候,只保證java.base模塊可以被編譯成AOT庫。
繼續參照JEP 295,創建java.base-list.txt文件,內容主要是排除一些編譯有問題的方法,具體內容參照原文。
然後執行命令:
jaotc -J-XX:+UseCompressedOops -J-XX:+UseG1GC -J-Xmx4g --compile-for-tiered --info --compile-commands java.base-list.txt --output libjava.base-coop.so --module java.base
在筆者的機器上(i7-6600U + 16G內存 + 256G NVMe SSD),排除上述方法之後,編譯時間大約為9分多鐘。
48878 methods compiled, 4 methods failed (497771 ms) Parsing compiled code (1126 ms) Processing metadata (15811 ms) Preparing stubs binary (0 ms) Preparing compiled binary (104 ms) Creating binary: libjava.base-coop.o (5611 ms) Creating shared library: libjava.base-coop.so (7306 ms) Total time: 542536 ms
完成之後,就可以使用AOT版本的java.base模塊:
java -XX:AOTLibrary=java_base/libjava.base-coop.so,./libHelloWorld.so HelloWorld
同樣,針對AOT,jvm也新增了參數打印哪些方法是通過加載AOT預編譯庫執行。
java -XX:+PrintAOT -XX:AOTLibrary=java_base/libjava.base-coop.so,./libHelloWorld.so HelloWorld
輸出可以和不使用java.base的AOT進行比較,發現不使用java.base的AOT庫,只能會加載libHelloWorld.so中對應的方法。
$ java -XX:+PrintAOT -XX:AOTLibrary=./libHelloWorld.so HelloWorld 11 1 loaded ./libHelloWorld.so aot library 105 1 aot[ 1] HelloWorld.()V 105 2 aot[ 1] HelloWorld.main([Ljava/lang/String;)V Hello World!
$ java -XX:+PrintAOT -XX:AOTLibrary=java_base/libjava.base-coop.so,./libHelloWorld.so HelloWorld 13 1 loaded java_base/libjava.base-coop.so aot library 13 2 loaded ./libHelloWorld.so aot library [Found [Z in java_base/libjava.base-coop.so] [Found [C in java_base/libjava.base-coop.so] [Found [F in java_base/libjava.base-coop.so] [Found [D in java_base/libjava.base-coop.so] [Found [B in java_base/libjava.base-coop.so] [Found [S in java_base/libjava.base-coop.so] [Found [I in java_base/libjava.base-coop.so] [Found [J in java_base/libjava.base-coop.so] 31 1 aot[ 1] java.lang.Object.()V 31 2 aot[ 1] java.lang.Object.finalize()V ...
輸出太長,節選部分輸出,我們可以看見java基礎類及其方法都通過AOT的方式進行加載。
實用嗎?
目前AOT的局限有:
AOT可能帶來的好處,是JVM加載這些已經預編譯成二進制庫之後,可以直接調用,而無需再將其運行時編譯成二進制碼。理論上,AOT的方式,可以減少JIT帶來的預熱時間,減少Java應用長期給人帶來的“第一次運行慢”感覺。
不過,本文使用的HelloWorld過於簡單,無法通過對比得出AOT是否可以減少JVM初始化時間。筆者嘗試對一個小型springboot應用進行AOT化,但是springboot框架本身無法在Java 9中運行。同時直接對spring-core的jar包執行jaotc也因為各種依賴問題而失敗。
經過各種嘗試,目前Java 9的AOT功能還處於很初步的階段:
期待Java 9正式發布的時候,能夠對AOT有更好的支持。