Java AOT 快取功能仍不成熟,部署風險與解決方案
Java's new AOT cache feature is in a rough state
Java AOT 快取 是 JDK 24 以來的功能,旨在提升啟動速度,但實際使用中存在多項陷阱。
- CPU 相容性:若訓練機器使用 AVX 等指令集,部署於缺乏相同指令集的機器時 JVM 會因
SIGILL崩潰。
此文章揭示 Java AOT 快取在實務部署時的陷阱與解決方案,對想提升應用啟動效能的開發者具有實際參考價值。
導言
AOT(提前編譯)快取是 JDK 最新的提升 Java 應用程式啟動與熱身時間的方法。概念很簡單:你先執行一次「訓練跑」你的應用程式,期間 JVM 會記錄程式做了什麼,例如載入、連結哪些類別以及呼叫哪些方法。之後再次啟動程式時,JVM 可以跳過大量初始化工作,因為已經完成,並立即擁有所有已載入並連結好的類別。
理論上,這是一個免費的啟動效能提升,無需改動任何應用程式。實務上,將其應用於像 XPipe 這類面向終端使用者的實際應用,卻有許多坑需要學習。最好的情況是因不相容而不會套用 AOT 快取,結果什麼都沒得到;最壞的情況則是若不小心,JVM 可能會意外崩潰。因此,雖然是免費的啟動加速,但僅在特定條件下有效,且還可能帶來免費的崩潰。
希望這篇文章能避免你在推行此功能時遇到我們曾遇到的相同問題。最後,你可以看到我們根據多年經驗所使用的建置指令碼。請注意,本文不包含任何效能測試或比較,因為那是另一個完整的話題。這篇文章已經相當長,內容也超出此處所涵蓋的範圍。
概念
若你已熟悉 JDK AOT 概念,可略過此節。若尚未熟悉,可先快速瞭解所有涉及 AOT 最佳化的相關部分。
時間線
自推出以來,AOT 快取隨每一次 JDK 版本發布而擴充,每一步都在一個JEP(JDK Enhancement Proposal)中說明:
- JDK 24:JEP 483:Ahead-of-Time Class Loading & Linking 引入了 AOT 快取本身
- JDK 25:JEP 514:Ahead-of-Time Command-Line Ergonomics 將快取建立簡化為單一步驟,使用
-XX:AOTCacheOutput,而 JEP 515:Ahead-of-Time Method Profiling 則將方法分析資料加入快取,讓 JIT 編譯器能更快熱身 - JDK 26:JEP 516:Ahead-of-Time Object Caching with Any GC 使快取物件能與任何垃圾回收器一起使用
- JDK 28:JEP 544:Ahead-of-Time Code Compilation 亦將編譯後程式碼加入快取
類別資料共用(CDS)
AOT 快取並非從零開始構建。它是類別資料共用(CDS)的自然演進,CDS 是 HotSpot 的一項功能,自 JDK 5 以來已存在。CDS 將已解析的類別元資料存於一個封存檔,JVM 在啟動時可直接對映至記憶體。隨著時間推移,CDS 在 JDK 10 加入了 application class-data sharing,在 JDK 12 之後每個 JDK 都隨附一個 default CDS archive,並於 JDK 13 引入 dynamic CDS archives。AOT 快取使用相同的基礎設施,這也是為什麼在使用 AOT 快取時,仍會在許多地方看到「shared archive」或 cds 日誌標籤等 CDS 用語。
AOT 類別連結
AOT快取與AOT類別鏈結經常被交替使用,但它們不是同一回事。AOT快取是檔案,可能包含不同種類的資料:已載入的類別資料(來自CDS的部分)、方法概況、快取的Java物件、部分原生程式碼以及預先鏈結的類別。AOT類別鏈結只是這些最佳化之一。當建立AOT快取時(-XX:+AOTClassLinking),它會自動啟用並將類別儲存為已鏈結狀態,讓JVM在啟動時可以跳過這個工作。你需要啟用類別鏈結,否則會失去啟動效能提升。
GraalVM Native Image
AOT快取也不應與 GraalVM Native Image 混淆。 Native Image 會將整個應用程式預先編譯成獨立的原生可執行檔,並在不使用 JVM 的情況下執行。 為了做到這點,它必須採用 封閉世界假設: 所有在執行時可能被執行的程式碼必須在建置時已知,因此像是反射、動態類別載入或資源等動態功能必須明確設定。 作為回報,你會得到非常快的啟動速度和低記憶體使用量。
AOT快取採用不同的做法。你的應用程式仍然在一般 JVM 上執行,使用 JIT 編譯器並保有 Java 的完整動態功能,因此不必改動或設定任何程式碼。快取僅加速 JVM 本來就需要做的工作。若無法使用,JVM 會回落到正常的執行時處理。相較於 Native Image,啟動效能提升較小,但你也不必面對其任何限制。若你曾經排除 GraalVM native image 的問題,應該對此很熟悉。
CPU incompatibilities
理論上,整個 AOT 快取概念聽起來像只會影響類別資料,並不進行任何原生編譯。若它真的進行編譯,當訓練系統與實際系統的個別 CPU 功能不一致時,即使 CPU 架構相同,也會出現各種問題。曾使用 GraalVM native image 並在缺少 CPU 功能的系統上遇到崩潰的人,應該對此以及 GraalVM 的建置引數 -march=compatibility 很熟悉。
實際上,帶有類別鏈結的 AOT 快取確實會產生少量原生程式碼。如果你的訓練系統支援 AVX 等功能,而執行系統不支援(或支援程度不同,例如不支援 AVX‑512,對應 JDK 的 -XX:UseAVX=3 等級),就會遇到這種後果。因為此時 JVM 會因以下訊息而崩潰:
A fatal error has been detected by the Java Runtime Environment:
SIGILL (0x4) at pc=0x00007fcc85ae2613, pid=4054, tid=4147
JRE version: OpenJDK Runtime Environment Zulu25.32+21-CA (25.0.2+10) (build 25.0.2+10-LTS)
Java VM: OpenJDK 64-Bit Server VM Zulu25.32+21-CA (25.0.2+10-LTS, mixed mode, sharing, tiered, compressed oops, compact obj headers, g1 gc, linux-amd64)
Problematic frame:
v ~AdapterBlob 0x00007fcc85ae2613
or
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ILLEGAL_INSTRUCTION (0xc000001d) at pc=0x000001d00d9f6c2b, pid=39700, tid=37844
#
# JRE version: OpenJDK Runtime Environment Zulu25.30+17-CA (25.0.1+8) (build 25.0.1+8-LTS)
# Java VM: OpenJDK 64-Bit Server VM Zulu25.30+17-CA (25.0.1+8-LTS, mixed mode, sharing, tiered, compressed oops, compact obj headers, g1 gc, windows-amd64)
# Problematic frame:
# v ~AdapterBlob 0x000001d00d9f6c2b
這意味著,例如你無法將在 13th Gen Intel Core CPU 上訓練的 AOT 快取應用程式,在擁有 12th Gen Intel Core CPU 的目標系統上執行,因為兩者在 AVX 的 CPU 功能支援層級不同。若你只使用更新世代 CPU,並將應用程式交付給仍使用舊世代硬體的客戶,將會得到不滿意的客戶,認為你甚至未對他們的軟體進行基本測試。
Adapter caches
原來它會產生介面卡快取,這是在 JDK 25 透過 JDK-8350209: Preserve adapters in AOT cache 新增的。介面卡是小段機器碼,用於在直譯與已編譯的 Java 呼叫之間切換(詳細說明請參見 HotSpot source)。它們是為建立快取的機器之 CPU 功能所產生,並未考慮應用程式將來可能在其他系統上執行。
一旦缺少快取介面卡所使用的 CPU 功能的系統呼叫其中之一,JVM 就會崩潰。這不僅是桌面應用程式分發給使用者時的問題。即使在後端伺服器上,AOT 快取也常在 CI 管道或容器映像建構時產生,然後部署到與建構機器 CPU 不同的機器,例如叢集中包含不同例項型別或雲端服務商將工作負載移至其他硬體時。
究竟為什麼這會是 AOT 快取的預設行為呢?
停用介面卡快取
原來你可以透過在 AOT 快取產生執行時傳遞引數 -XX:+UnlockDiagnosticVMOptions -XX:-AOTAdapterCaching 來停用 AOT 介面卡快取。這個選項並未在任何地方說明或檔案化,因此你必須閱讀 JDK 原始碼才能瞭解。更令人困惑的是,這個選項在 預設為 false。然而,如果快取包含 AOT 連結類別,AOT 快取產生實作會自動啟用它,請參見 JDK 25 的這裡。你可以用 -XX:+PrintFlagsFinal 看到,組譯執行期間值為 true {ergonomic}。因此它在預設情況下是停用的,除非是預設案例。
若你使用 AOT 快取,傳遞這些引數基本上是必須的,否則使用者會遇到崩潰。此問題最終被報告為 JDK-8388703。JDK 27 將改為 將訓練機器的 CPU 功能儲存於 AOT 快取的原生碼部分,該部分目前包含介面卡,並且 僅在 CPU 功能完全匹配時使用快取的介面卡。
AOT 程式碼快取實際是什麼
更讓人困惑的是,「AOT 程式碼快取」這個術語實際上有兩個不同的含義。他們說電腦科學中只有兩件難事:快取失效和命名,這個術語卻同時涉及兩者。在 JDK 原始碼和日誌中,AOT 程式碼快取(AOTCodeCache)是 AOT 快取中包含原生碼的部分。自 JDK 25 起已存在,且目前包含上述介面卡。因此當你看到類似 AOT Code Cache is not used: AOT Class Linking is not used 或 AOT Code Cache disabled: cpu features are incompatible 的日誌訊息,或閱讀關於 AOT 程式碼快取的錯誤報告時,主要是關於介面卡,而非已編譯的 Java 程式碼。它也不是常規程式碼快取,即 JIT 編譯器在執行時存放已編譯程式碼的地方。上述 JDK 27 中提到的 CPU 功能檢查也是此一部分。實際的 Java 程式碼預先編譯(大多數人聽到 AOT 程式碼快取時會想到的)將在 JDK 28 出現,並將編譯好的程式碼存於同一個 AOT 程式碼快取中。
JDK 28 中的 AOT 程式碼編譯
JDK 28 將推出額外功能,JEP 544:先期編譯 (Ahead-of-Time Code Compilation),如名稱所示,亦會對你的快取進行原生程式碼編譯,編譯內容不只是介面卡。根據 JEP,若 CPU 功能不匹配,AOT 程式碼將不會被載入,至少能避免崩潰。此功能使用與 JDK 27 相同的 CPU 功能檢查,要求完全匹配:在 x86 上,除超執行緒之外的所有 CPU 功能必須相同。因此,即使是比訓練機器支援更多功能的新 CPU,也不會載入 AOT 程式碼。
然而,這也會使此功能對於分發給使用者的應用程式變得相當無用,因為使用者的 CPU 與你建置機器的 CPU 完全相同的機率相當低。所需的是某種相容性開關,能停用所有可選的 CPU 功能。當然,這會降低編譯程式碼的效能,但可以換取更快的啟動時間。技術上,比較的 CPU 功能是 JVM 啟用的功能,可透過類似 -XX:UseAVX 的選項減少(參見此處)。但你必須將相同的選項傳給訓練執行與實際應用執行,而且並非所有 CPU 功能都有此類選項,因此這也不是一個實際解決方案。此處需要一個適當的相容性模式開關。
擷取日誌
處理 AOT 快取時,JVM 引數 -Xlog:aot 是你最好的夥伴。當然,這種夥伴會用謎語回答每個問題。它在生成 AOT 快取時會印出詳細資訊,也會在執行已建置的應用程式時顯示 AOT 快取是否正確載入。然而,輸出相當難以理解且易被誤解,存在多個陷阱。
兩次執行闖入日誌
JEP 514:先期命令列人體工學 (Ahead-of-Time Command-Line Ergonomics) 引入了更簡易的 AOT 快取構建工作流程。你不再需要執行兩個步驟,現在只需一次呼叫應用程式即可構建 AOT 快取。然而,技術上它仍會執行兩次獨立的 JVM 執行,只是隱藏了這一事實。因此,你會將兩次呼叫的日誌視為一次,只有第二次,即實際寫入 AOT 快取的組裝執行,告訴你快取中實際存放的內容。兩者看起來非常相似。
若你不知情,將會花費大量時間除錯錯誤的日誌,直到發現有兩個獨立的日誌相繼輸出,而只有後者與快取內容相關。因此,使用 CTRL+F 時請小心,並確認你正在檢視正確版本的日誌。
令人困惑的日誌訊息
此外,日誌訊息基本上缺少所有相關資訊,且幾乎沒有檔案可作佐證。例如,組裝 AOT 快取的執行會記錄類似以下內容:
[0.024s][info][aot] full module graph: disabled
[0.024s][info][aot] initial optimized module handling: enabled
[0.024s][info][aot] initial full module graph: disabled
[0.024s][info][aot] Using AOT-linked classes: false (static archive: no aot-linked classes)
[0.032s][info][aot] JVM_StartThread() ignored: java.lang.ref.Reference$ReferenceHandler
[0.032s][info][aot] JVM_StartThread() ignored: java.lang.ref.Finalizer$FinalizerThread
[0.050s][info][aot] Not starting management agent during creation of AOT cache.
現在,你的第一個問題可能是:什麼是完整模組圖?為何被停用?如何啟用?這會影響 AOT 快取的效能嗎?所有答案都必須參考 JDK 原始碼,因為它幾乎未作說明。為何某個功能被停用也未作說明,因此你還需更深入挖掘。停用功能的影響亦未被檔案化。
在 java manpage 中包含的少量相關資訊仍列於 CDS 區段,而非實際的 Ahead‑of‑Time Cache 區段。因此你必須先知道要檢視哪裡。但這些說明是以 CDS 為前提撰寫的,因此你仍需將其轉譯為 AOT cache 並瞭解哪些部分適用。無論如何,對此而言相關資訊實際上只有少量。
更令人困惑的是,以上的 full module graph: disabled 行會在每一次組裝執行時印出,即使系統完全正常。是否完整模組圖與 AOT 類別連結實際上能正常工作,只能在日誌最終看到,例如 Full module graph = enabled 行與類別摘要 instance classes = 9033, aot-linked = 9033, ...。若有錯誤,則會在中間出現類似 archivedBootLayer not available, disabling full module graph 的錯誤訊息,並伴隨 aot-linked = 0。因此,一些關於功能被停用的訊息可以忽略,而看起來相似的其他訊息則是相關的。
總結來說,幾乎所有內容都缺乏檔案,日誌訊息不直觀,且 AOT 的工作流程不透明。即使可靠地瞭解發生了什麼,也需要花費大量時間。
脆弱的類別連結
AOT 類別連結是 AOT cache 最脆弱的部分,因為它需要將你應用程式的完整模組設定(即所謂的完整模組圖)也存入快取,且許多條件會悄悄停用它。若發生此情況,AOT cache 仍會被建立並使用,但類別必須在執行階段再次連結,因而失去部分啟動最佳化。因此,即使 AOT cache 成功載入,也不一定表示類別連結成功。
不幸的是,在組裝 AOT cache 時,AOT 類別連結功能似乎會盡力讓你失敗並在偵測到任何機會時停用所有最佳化,往往不會告訴你原因。對於不相容的 JVM 選項,你至少會收到類似 optimized module handling: disabled due to incompatible property: ... 的日誌訊息(在 JDK 24 只會搭配 -Xlog:cds,而非 -Xlog:aot,可能有人忘了重新命名日誌標籤)。在以下其他範例中,日誌並未說明為何被停用。
模組選項
JDK 24 原始版的 AOT cache 甚至不允許你為需要反射的情況指定 --add-opens - --add-opens、--add-exports 與 --add-reads 都被視為不相容,導致完整模組圖被停用,進而停用 AOT 類別連結。至少這在 JDK 25 已修正。
GraalVM
若你使用其他 JDK 實作,例如 GraalVM,將會遇到更多問題。GraalVM 包含額外模組,例如 jdk.internal.vm.ci,在標準 JDK 實作中找不到,這些模組透過 JVMCI(Java‑Level JVM Compiler Interface)提供進階 JIT 變通。此模組在執行階段存在,但在 GraalVM 使用其原生 JIT 編譯庫時,並不會自動加入你應用程式的模組圖(參見 此處)。因此,你必須從日誌中找出為何在使用 GraalVM 建構 AOT cache 時最佳化被停用,僅為發現你需要為 AOT 訓練執行做 --add-modules jdk.internal.vm.ci 以修正模組圖不一致。
最終欄位突變警告
接下來,你需要小心啟用哪些 JVM 引數。使用 JDK 26 時,若透過反射修改 final 欄位,會因 JEP 500:準備讓 Final 真正是 Final 而產生警告。理論上這是好主意,但實際上如果某個相依套件觸發,像是某些 Jackson 的小把戲,會相當煩人。為了消除這些警告,你需要傳遞 --enable-final-field-mutation=...。然而該選項又會 再次停用最佳化模組處理(--illegal-final-field-mutation 與 --illegal-native-access 亦同),所以問題與往常相同。其實也沒有技術原因必須這麼做,為何不讓我自行冒風險使用呢?目前情況是,要麼是第三方相依套件非法變更的執行時警告,要麼就沒有類別連結。
孵化模組
各種模組配置會停用最佳化(當然沒檔案說明,必須自行調查)。例如,若使用 JDK 或 JavaFX 的任何孵化模組,將無法進行 AOT 類別連結。JavaFX 的孵化模組與 JDK 模組使用相同的孵化旗標。如果模組圖中包含任何孵化模組,boot 層就不會被封存,這會停用完整的模組圖與類別連結。這大概只是因為封存的 boot 層會跳過模組系統初始化,而 會印出煩人的孵化模組警告,並非深層技術原因。加上加入孵化的 JavaFX richtext 模組後,類別連結忽然被停用,還花了不少時間才弄清楚原因。
基準測試影響
所有這些問題使得比較變得極其困難。若開發者必須如此謹慎,以免某些最佳化被停用,怎能評估此功能的效益?剛才我看到有人發布使用 GraalVM 產生的 AOT 基準測試,我心想:除非那人真的檢查了 aot 日誌並理解所有模糊訊息,否則他可能根本沒意識到 GraalVM 的不同模組清單已停用許多最佳化。
許多其他陷阱亦相同。你在 JDK25 為應用程式精心設定 AOT,升級至 JDK26 後看到這些煩人的非法變更警告,便直接停用,因為為什麼不呢?但這樣做時,你很可能連 AOT 類別連結已消失都未察覺。
亦無法在類別連結被停用時觸發失敗,以便偵測。若你想讓 CI pipeline 在任何原因導致 AOT 類別連結被停用時失敗,則無法做到。因此你無法輕易捕捉到此問題。
我的 AOT 快取在哪?
以往,使用 CDS 時,JVM 找不到資料從未成為問題。預設的 CDS 壓縮檔位於 JDK 內固定位置(lib/server/classes.jsa,或 Windows 上的 bin\server\classes.jsa),或可按需求自訂位置。當時僅有一個相關的 JVM 執行環境。
使用 AOT 後,現在有多個獨立的 JVM 執行:訓練執行、快取組裝執行,以及實際執行應用程式的目標系統執行。對於更複雜的應用程式,甚至可能有多個不同的 AOT 快取,例如你透過 jlink 建立多個啟動器,或透過 jpackage 建立多個原生可執行檔。訓練與組裝執行會透過 -XX:AOTCacheOutput 取得快取位置,但對於實際執行你的應用程式,必須用 -XX:AOTCache 告訴 JVM 快取資料所在位置。否則它不會載入,亦無預設值或自動查詢。
相對路徑
這其實在處理相對檔案路徑時並不那麼容易,這基本上就是你必須使用該選項的方式。絕對檔案路徑對 AOT 快取來說並無意義,因為你的應用程式可能會被放置在目標系統的任意目錄位置。你可以為 -XX:AOTCache 指定相對路徑,但這個路徑是相對於工作目錄,而非你的可執行檔。任何在不同工作目錄執行你的應用程式的人都會破壞 AOT 快取。
jpackage 安裝佈局
當你透過 jlink 和 jpackage(這是現代 JDK 的預期方式)為你的應用程式產生執行時映像時,你還必須處理各平臺不同的安裝佈局,因此你需要平臺特定的 AOT 快取位置。你還需要知道如何以相對於安裝根目錄的方式指定路徑(也可以透過瀏覽 JDK 原始碼來查詢,因為 jpackage 檔案並未提及)。總結來說,你需要在 jpackage 的應用程式 .cfg 檔案中這樣指定 AOT 快取位置:
- Windows:
java-options=-XX:AOTCache=$ROOTDIR/runtime/bin/server/cache.aot - Linux:
java-options=-XX:AOTCache=$APPDIR/../../lib/runtime/lib/server/cache.aot - macOS:
java-options=-XX:AOTCache=$ROOTDIR/Contents/runtime/Contents/Home/lib/server/cache.aot
變數 $ROOTDIR 和 $APPDIR 在啟動時會被 jpackage 啟動器自動替換。$ROOTDIR 在 Linux 上效果不佳,因此使用 $APPDIR 作為變通方案(這又是另一個故事)。
稍後產生
AOT 快取不必在生產環境中第一次啟動你的應用程式之前生成。它也可以像 CDS 一樣在之後動態生成。如果實際快取檔案的 AOTCache 選項處理能夠改進,那也可以做到。
但目前,在目標系統上將新生成的 AOT 快取放到正確位置,並考慮唯讀安裝目錄,並不簡單。沒有預設的檔案路徑。你不能在使用者的目標系統上使用固定的絕對路徑。對於 -XX:AOTCache 檔案也無法使用環境變數。你可以傳遞相對檔案路徑,但只能相對於工作目錄。若安裝目錄為唯讀或你缺少許可權,就無法在你想要的位置生成 AOT 快取。
EDR 的困境
當然,透過 JVMTI(ClassFileLoadHook)掛鉤到類別載入的代理程式也會阻止 AOT 快取被載入,如果它包含 AOT 連結類別,這並不意外,根據先前說明的例子可知 AOT 快取非常脆弱。然而,在實務上,這會帶來更多意外問題。
如果此類代理程式在啟動時被附加,AOT 快取將無法使用,你會收到類似以下的錯誤訊息:
[error][aot] AOT cache has aot-linked classes. It cannot be used when JVMTI ClassFileLoadHook is in use.
[error][aot] An error has occurred while processing the AOT cache.
[error][aot] Unable to map shared spaces
這不是終端錯誤,那到底有什麼大不了?其實這可能會無意間破壞你的應用程式輸出,因為這些錯誤會輸出到 stdout,而非 stderr。例如,SentinelOne 會在任何啟動的 Java 應用程式上附加代理程式,因此它們預設會一直輸出此錯誤。
如果使用者依賴你的應用程式輸出,例如執行 API_KEY=$(myapp generate-key),他們的工作流程會在執行嘗試附加代理程式的 EDR 或類似工具時中斷。因為突然之間,你指令的常規單行輸出變成多行,最上面還帶著 AOT 錯誤。
你實際上必須在生產版本中以 -Xlog:disable 方式執行你的應用程式。或是你可以做類似 -Xlog:disable -Xlog:all=warning:stderr 的操作,將其保留在 stderr,警告與錯誤通常應該在那裡。
綜合整理
為了讓您瞭解所有這些如何協同工作,以下是我們用來建立 AOT 快取的 Gradle 任務簡化版本。它在 jlink 與 jpackage 為目前平臺建立應用程式映像後執行,並在將映像打包成安裝程式之前將 AOT 快取加入其中。
def distDir = "${project.layout.buildDirectory.get()}/dist"
tasks.register('trainAot', DefaultTask) {
doLast {
// You usually want your application to behave a bit differently
// during a training run, so we pass properties to do that
// E.g. it should not block with a dialog if it is a GUI application
// as it should run unattended
def properties = [
"-D" + propertyName("dataDir") + "=$distDir/local_train",
"-D" + propertyName("aotTrainMode") + "=true",
]
def jvmName = System.getProperty("java.vendor.version")
def graal = jvmName != null && jvmName.toLowerCase().contains("graal")
// The generated jlink/jpackage image
def imageDir
// The command to launch the jlink launcher script
def command
// The cache output location is relative to the working directory of the jlink launcher script
def cacheOutput
// The jpackage app configuration file
def cfgFile
// The AOT cache location as seen from the jpackage launcher configuration
def cfgCache
if (os.isWindows()) {
// We generated the application image here during the build
imageDir = "$distDir/${productName}"
command = ['cmd', '/c', "$imageDir/runtime/bin/${jpackageExecutableName}.bat"]
cacheOutput = './server/cache.aot'
cfgFile = "$imageDir/app/${jpackageExecutableName}.cfg"
cfgCache = '$ROOTDIR/runtime/bin/server/cache.aot'
} else if (os.isLinux()) {
// We generated the application image here during the build
imageDir = "$distDir/${productName}"
command = ["$imageDir/lib/runtime/bin/${jpackageExecutableName}"]
cacheOutput = './lib/runtime/lib/server/cache.aot'
cfgFile = "$imageDir/lib/app/${jpackageExecutableName}.cfg"
cfgCache = '$APPDIR/../../lib/runtime/lib/server/cache.aot'
} else {
// We generated the application image here during the build
imageDir = "$distDir/${productName}.app"
command = ["$imageDir/Contents/runtime/Contents/Home/bin/${jpackageExecutableName}"]
cacheOutput = './Contents/runtime/Contents/Home/lib/server/cache.aot'
cfgFile = "$imageDir/Contents/app/${jpackageExecutableName}.cfg"
cfgCache = '$ROOTDIR/Contents/runtime/Contents/Home/lib/server/cache.aot'
}
def jvmOptions = [
"-XX:+UnlockDiagnosticVMOptions",
"-XX:-AOTAdapterCaching",
"-XX:+AOTClassLinking",
"-XX:AOTCacheOutput=$cacheOutput",
"-Xlog:aot",
]
if (graal) {
jvmOptions += ["--add-modules", "jdk.internal.vm.ci"]
}
execOps.exec {
commandLine((command + properties).collect { it.toString() })
workingDir imageDir
// We use the old variable CDS_JVM_OPTS of the default jlink launcher scripts
// to pass custom AOT JVM arguments to the process
// All jlink launcher scripts support this variable
environment "CDS_JVM_OPTS", jvmOptions.join(" ")
}
def cfg = file(cfgFile)
cfg.text = cfg.text + "java-options=-XX:AOTCache=$cfgCache\njava-options=-Xlog:disable\n"
}
}
訓練執行使用訓練模式屬性和獨立的資料目錄,避免幹擾現有的應用程式資料。在訓練模式下,我們的應用程式會執行固定工作負載,例如開啟使用者介面的所有不同頁面,然後自行結束。這樣就能確保訓練執行涵蓋正常會話中使用的類別與程式碼路徑。
結論
總而言之,使 AOT 快取正常運作且沒有副作用的難度遠超預期。此功能脆弱,除 JEP 之外幾乎沒有檔案,日誌往往提出更多問題而非答案。本文的大部分知識只能透過閱讀 JDK 原始碼獲得。
這就提出了問題:JEP 真的算作檔案嗎?當所說的功能多次更新(例如 AOT 快取功能)時,原始 JEP 會被更新嗎?我把 JEP 視為對新提案功能的簡短摘要,主要聚焦於理想情境。這些頁面的目的是說服人們此功能必要,而非詳細說明。我覺得每當有人告訴你去查閱 JEP 作為檔案時,這裡就有問題。
此功能也未能充分考慮實際應用的挑戰。將 AOT 功能建立在 CDS 之上顯然無幫助,因為兩者在實際使用案例上已相距甚遠。或許單獨實作會更好。但即使如此,整個 CPU 功能相關的崩潰問題被納入 JDK25 版,顯示規劃與準備不足。由於這並非真正的錯誤,而是規劃不周的功能,似乎每個人都忘了:當某物提前生成時,實際 CPU 功能可能並且往往會不同。這也偏離了實際的 JEP,JEP 並未將此列為潛在風險或考量。唯一能以此形式實作的方式是此功能從未被考慮或測試。
不過,這篇關於 AOT 快取的文章實在太長了,我們甚至還沒提到效能比較!這可能是另一篇文章的主題。