
40-GraalVM與AOT編譯引言前幾篇我們深入了HotSpot JIT的各項(xiàng)運(yùn)行時(shí)優(yōu)化。JIT的精髓是運(yùn)行時(shí)收集profile、自適應(yīng)優(yōu)化但它有一個(gè)固有矛盾優(yōu)化需要時(shí)間累積啟動(dòng)期必然慢。對(duì)長(zhǎng)跑的服務(wù)端應(yīng)用這點(diǎn)啟動(dòng)開(kāi)銷可以忽略但對(duì)Serverless、CLI工具、函數(shù)計(jì)算這類啟動(dòng)即用完的場(chǎng)景JIT的預(yù)熱成了致命短板。**AOT編譯Ahead-of-Time Compilation**是另一條路在程序運(yùn)行前就把字節(jié)碼編譯成機(jī)器碼啟動(dòng)即峰值。GraalVM正是這條路上的旗艦項(xiàng)目。本篇將梳理Graal編譯器、jaotc工具、GraalVM Native Image的原理與取舍并對(duì)比C1/C2/Graal/Native Image四種編譯路徑最后討論Spring Native的實(shí)踐。這是JVM原理詳解專欄即時(shí)編譯模塊的收官篇。Graal編譯器用Java寫(xiě)的JITGraal首先是一個(gè)JIT編譯器用純Java編寫(xiě)可作為HotSpot中C2的替代品。它在JDK 10作為實(shí)驗(yàn)特性引入-XX:UseGraalJIT背后是Oracle Labs的長(zhǎng)期投入。Graal的架構(gòu)特點(diǎn)Graal與C2一樣基于Sea-of-Nodes IR但實(shí)現(xiàn)完全用Java┌─────────────────────────────────────┐ │ HotSpot JVM (C runtime) │ │ ┌─────────────────────────────┐ │ │ │ 字節(jié)碼 → Graal IR │ │ │ │ ↓ 優(yōu)化遍 │ │ │ │ Graal IR → 機(jī)器碼 │ │ │ └─────────────────────────────┘ │ │ ↑ Graal本身是Java代碼 │ │ ┌─────────────────────────────┐ │ │ │ Graal編譯器 (Java) │ │ │ │ 運(yùn)行在JVM上 │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘這種用Java寫(xiě)Java編譯器的設(shè)計(jì)帶來(lái)幾個(gè)優(yōu)勢(shì)可維護(hù)性相比C2的C代碼Graal的代碼更現(xiàn)代、模塊化便于演進(jìn)。C2經(jīng)過(guò)二十多年迭代代碼高度復(fù)雜、耦合度高新開(kāi)發(fā)者上手困難可擴(kuò)展插件化優(yōu)化遍開(kāi)發(fā)者可用Java寫(xiě)自定義優(yōu)化Truffle框架正是基于此與GraalVM生態(tài)復(fù)用同一套Graal編譯器既可作JIT也可作AOT是GraalVM的技術(shù)底座啟用Graal作為JIT# JDK 17實(shí)驗(yàn)特性java-XX:UnlockExperimentalVMOptions-XX:UseGraalJITMyApp注意Graal作為JIT需要JVM本身先運(yùn)行起來(lái)——它本身是Java代碼需要JVM承載。這帶來(lái)一個(gè)雞生蛋問(wèn)題JVM啟動(dòng)時(shí)Graal還沒(méi)就緒所以早期代碼仍由解釋器C1處理等Graal就緒后才接手熱點(diǎn)方法的編譯。分層編譯在這里依然發(fā)揮作用。Graal vs C2性能上Graal在某些基準(zhǔn)特別是Scala、Twitter風(fēng)格的服務(wù)端代碼上能與C2持平甚至略優(yōu)但通用場(chǎng)景未必明顯勝出。Graal的編譯速度通常比C2慢畢竟它本身是Java應(yīng)用有JIT預(yù)熱問(wèn)題。目前Graal在標(biāo)準(zhǔn)HotSpot中仍是可選實(shí)驗(yàn)特性生產(chǎn)環(huán)境主流仍是C2。Graal更大的價(jià)值在于它是Native Image的技術(shù)基礎(chǔ)。AOT編譯jaotc工具**AOT編譯Ahead-of-Time Compilation**指在程序運(yùn)行前將字節(jié)碼編譯成機(jī)器碼。JDK 9引入了jaotc工具JDK 10~17持續(xù)維護(hù)但JDK 17后逐漸被GraalVM Native Image路線取代。jaotc的工作方式j(luò)aotc將指定類或JAR的字節(jié)碼編譯成共享庫(kù).so/.dllJVM啟動(dòng)時(shí)加載這個(gè)庫(kù)相關(guān)方法直接執(zhí)行機(jī)器碼跳過(guò)解釋和JIT編譯。# JDK 17jaotc--outputlibapp.so--jarmyapp.jar# 運(yùn)行時(shí)加載AOT庫(kù)java-XX:AOTLibrary./libapp.so MyAppjaotc的局限jaotc并非真正的全AOT只編譯指定類未指定的類仍走解釋JITAOT代碼與JIT代碼混合執(zhí)行依賴平臺(tái)AOT庫(kù)與OS/架構(gòu)綁定不能跨平臺(tái)違背Java一次編寫(xiě)到處運(yùn)行的理念保守優(yōu)化沒(méi)有運(yùn)行時(shí)profile無(wú)法做speculative optimization類層次分析也只能基于編譯時(shí)已加載的類優(yōu)化程度不如C2維護(hù)成本高JDK團(tuán)隊(duì)評(píng)估后認(rèn)為jaotc的收益不抵維護(hù)成本JDK 17后基本停止演進(jìn)JDK 18起移除推薦用GraalVM Native Image替代jaotc的歷史意義在于驗(yàn)證了Java可以AOT的可行性但它的工程價(jià)值有限生產(chǎn)環(huán)境幾乎無(wú)人使用。GraalVM Native Image閉世界分析GraalVM Native Image是GraalVM項(xiàng)目的核心功能也是目前Java生態(tài)最成熟的AOT方案。它不是把部分方法AOT化而是把整個(gè)Java應(yīng)用編譯成一個(gè)獨(dú)立的本地可執(zhí)行文件。閉世界分析Closed-World AnalysisNative Image的關(guān)鍵是閉世界分析在編譯時(shí)編譯器必須知道程序可能用到的所有類、方法、字段。這與標(biāo)準(zhǔn)JVM的開(kāi)放世界運(yùn)行時(shí)動(dòng)態(tài)加載截然不同。標(biāo)準(zhǔn)JVM運(yùn)行時(shí)開(kāi)放世界 類加載是動(dòng)態(tài)的反射、SPI、動(dòng)態(tài)代理可在運(yùn)行時(shí)引入新類 → JIT看到什么優(yōu)化什么未知的留給運(yùn)行時(shí) Native Image編譯時(shí)封閉世界 從main方法出發(fā)靜態(tài)分析所有可達(dá)的代碼路徑 → 必須窮盡所有可能執(zhí)行的類否則運(yùn)行時(shí)ClassNotFound閉世界分析讓Graal能做極其徹底的優(yōu)化——整個(gè)程序的調(diào)用圖是已知的可以全局內(nèi)聯(lián)、全局死代碼消除、移除所有未用到的類庫(kù)代碼“Reachability Analysis”。一個(gè)引入了龐大依賴鏈的應(yīng)用最終產(chǎn)出的可執(zhí)行文件只包含真正用到的代碼體積小、啟動(dòng)快。編譯流程# 安裝GraalVM后native-image--jarmyapp.jar-omyapp# 產(chǎn)出 myapp 可執(zhí)行文件./myappNative Image的編譯過(guò)程大致是入口分析從main方法出發(fā)標(biāo)記所有可達(dá)的方法可達(dá)性分析遞歸追蹤方法體內(nèi)的字段訪問(wèn)、方法調(diào)用、類初始化擴(kuò)展可達(dá)集合反射配置處理對(duì)反射、動(dòng)態(tài)代理等動(dòng)態(tài)特性需提供配置文件指明哪些類/方法會(huì)被反射訪問(wèn)編譯對(duì)可達(dá)代碼做AOT編譯生成機(jī)器碼這里用的就是Graal編譯器鏈接與GraalVM運(yùn)行時(shí)SubstrateVM鏈接產(chǎn)出可執(zhí)行文件運(yùn)行時(shí)特性Native Image產(chǎn)出的可執(zhí)行文件運(yùn)行在SubstrateVM上這是一個(gè)精簡(jiǎn)的Java運(yùn)行時(shí)無(wú)JIT代碼已AOT編譯運(yùn)行時(shí)不再編譯也不做speculative optimization獨(dú)立GC自帶Serial GC或G1可選不依賴HotSpot的GC無(wú)解釋器所有代碼都是機(jī)器碼單線程啟動(dòng)啟動(dòng)時(shí)無(wú)需JVM預(yù)熱毫秒級(jí)就緒線程本地堆部分版本支持進(jìn)一步降低分配開(kāi)銷啟動(dòng)速度與內(nèi)存占用Native Image的核心價(jià)值在于啟動(dòng)快、內(nèi)存少。典型對(duì)比指標(biāo)標(biāo)準(zhǔn)JVMNative Image啟動(dòng)時(shí)間數(shù)百ms~數(shù)秒幾ms~幾十ms內(nèi)存占用100MB10~30MB峰值性能高C2優(yōu)化中等無(wú)profile優(yōu)化保守首請(qǐng)求延遲高預(yù)熱中低即峰值這組特性讓Native Image非常適合Serverless、CLI工具、微服務(wù)冷啟動(dòng)敏感場(chǎng)景。AOT的代價(jià)喪失動(dòng)態(tài)性AOT的收益不是免費(fèi)的最大的代價(jià)是喪失Java的動(dòng)態(tài)性。閉世界分析與Java的動(dòng)態(tài)特性天然沖突。反射的限制Java的反射允許運(yùn)行時(shí)按字符串名加載類、調(diào)用方法。閉世界分析無(wú)法靜態(tài)確定這些字符串的值Class?clazzClass.forName(props.getProperty(impl));Objectobjclazz.getDeclaredConstructor().newInstance();impl屬性的值來(lái)自配置文件編譯時(shí)未知。Native Image不知道要把哪個(gè)類納入可達(dá)集合運(yùn)行時(shí)就會(huì)ClassNotFoundException。解決方案是提供反射配置文件顯式聲明哪些類會(huì)被反射訪問(wèn){name:com.example.MyImpl,allDeclaredConstructors:true,allDeclaredMethods:true}Native Image根據(jù)配置把這些類納入可達(dá)集合。但這要求開(kāi)發(fā)者提前知道所有反射目標(biāo)違背了反射運(yùn)行時(shí)發(fā)現(xiàn)的初衷。動(dòng)態(tài)代理與字節(jié)碼增強(qiáng)Proxy.newProxyInstance需配置文件聲明代理接口否則生成的代理類不在可達(dá)集合CGLIB/ByteBuddy運(yùn)行時(shí)生成字節(jié)碼Native Image無(wú)法處理運(yùn)行時(shí)生成的類需改為編譯時(shí)增強(qiáng)或AOT處理MethodHandle/LambdaMetafactory部分支持但有約束類加載器與SPIJava的SPI如ServiceLoader依賴運(yùn)行時(shí)類加載。Native Image需要在編譯時(shí)枚舉所有實(shí)現(xiàn)類通過(guò)META-INF/services配置納入。復(fù)雜的類加載器隔離如OSGi、Tomcat的WebappClassLoader基本無(wú)法直接AOT因?yàn)檫@些類加載器的核心價(jià)值就是運(yùn)行時(shí)動(dòng)態(tài)加載。動(dòng)態(tài)配置與配置文件處理這些動(dòng)態(tài)特性的工具是配置文件# 運(yùn)行期agent收集反射、動(dòng)態(tài)代理等使用情況java-agentlib:native-image-agentconfig-output-dirMETA-INF/native-image/-jarmyapp.jar# 基于收集到的配置做Native Imagenative-image--jarmyapp.jarnative-image-agent會(huì)在應(yīng)用運(yùn)行時(shí)記錄所有反射、資源加載、動(dòng)態(tài)代理、JNI等操作生成配置文件供AOT使用。這是當(dāng)前主流的工程實(shí)踐——先跑一次應(yīng)用收集profile再AOT編譯。但要注意agent只能收集到這次運(yùn)行觸達(dá)的路徑未覆蓋的分支仍會(huì)在生產(chǎn)中崩潰必須配合完整測(cè)試。C1 / C2 / Graal / Native Image 對(duì)比特性C1C2Graal(JIT)Native Image編譯時(shí)機(jī)運(yùn)行時(shí)運(yùn)行時(shí)運(yùn)行時(shí)運(yùn)行前(AOT)語(yǔ)言CCJavaJava優(yōu)化深度淺深深深但保守(無(wú)profile)啟動(dòng)性能中(快速介入)慢(需預(yù)熱)慢(需預(yù)熱)極快(即峰值)峰值性能中高高中(無(wú)speculative)內(nèi)存占用中中中低動(dòng)態(tài)性支持完全完全完全受限適用場(chǎng)景桌面/短任務(wù)服務(wù)端長(zhǎng)跑實(shí)驗(yàn)性/服務(wù)端Serverless/CLI/微服務(wù)默認(rèn)啟用分層編譯一部分是否(實(shí)驗(yàn))否(需GraalVM)這張表揭示了關(guān)鍵取舍長(zhǎng)跑服務(wù)用C2標(biāo)準(zhǔn)HotSpot峰值性能最優(yōu)speculative optimization讓它在穩(wěn)態(tài)下幾乎無(wú)敵啟動(dòng)敏感場(chǎng)景用Native Image犧牲峰值換啟動(dòng)毫秒級(jí)就緒Graal作為JIT目前仍是實(shí)驗(yàn)性未來(lái)可能替代C2但短期內(nèi)不會(huì)默認(rèn)啟用——C2的成熟度和穩(wěn)定性仍不可替代Spring Native與AOT處理Spring Framework 6 / Spring Boot 3正式支持Spring Native讓Spring應(yīng)用能被GraalVM Native Image編譯。這是Java生態(tài)擁抱AOT的標(biāo)志性事件。Spring Native的挑戰(zhàn)Spring Framework大量使用反射、動(dòng)態(tài)代理、配置注入這些都與AOT沖突。直接用native-image編譯Spring應(yīng)用會(huì)因反射目標(biāo)缺失而失敗或運(yùn)行時(shí)崩潰。Spring的Configuration、Bean、條件化裝配等機(jī)制本就依賴運(yùn)行時(shí)反射和字節(jié)碼增強(qiáng)。Spring的AOT處理方案Spring Native不依賴運(yùn)行時(shí)agent收集配置而是在編譯時(shí)做AOT處理Spring AOT插件在Maven/Gradle構(gòu)建時(shí)執(zhí)行靜態(tài)分析分析Bean定義、配置元數(shù)據(jù)確定所有需要反射的類生成AOT元數(shù)據(jù)產(chǎn)出META-INF/native-image/*.json配置文件生成Bean注冊(cè)代碼用代碼生成替代運(yùn)行時(shí)反射注入直接產(chǎn)生BeanFactoryInitializationAotContribution等代碼條件化裝配前移把Conditional等運(yùn)行時(shí)決策前移到編譯時(shí)編譯期就確定哪些Bean生效# Spring Boot 3 GraalVMmvn-Pnativenative:compile# 產(chǎn)出 target/myapp可執(zhí)行文件./target/myappSpring Native的收益與代價(jià)收益啟動(dòng)時(shí)間從秒級(jí)降到毫秒級(jí)典型Spring Boot應(yīng)用從5秒降到0.1秒內(nèi)存占用降到原來(lái)的1/5~1/3首請(qǐng)求延遲接近零無(wú)需預(yù)熱代價(jià)峰值吞吐比JVM模式低無(wú)JIT speculative優(yōu)化典型低20%~40%構(gòu)建時(shí)間長(zhǎng)AOT分析編譯從幾十秒漲到幾分鐘動(dòng)態(tài)特性受限運(yùn)行時(shí)Class.forName、運(yùn)行時(shí)字節(jié)碼增強(qiáng)不再可用配置復(fù)雜性增加需處理反射配置、資源配置等適用場(chǎng)景Spring Native適合Serverless / FaaS冷啟動(dòng)是核心指標(biāo)CLI工具命令行工具要秒開(kāi)微服務(wù)規(guī)模極大成百上千實(shí)例省內(nèi)存等于省錢(qián)Kubernetes Job/Pod頻繁創(chuàng)建銷毀啟動(dòng)快減少資源浪費(fèi)不適合長(zhǎng)跑的批處理/大數(shù)據(jù)峰值性能更重要重度依賴運(yùn)行時(shí)動(dòng)態(tài)特性的應(yīng)用遷移成本高需要熱部署/熱更新的場(chǎng)景AOT后無(wú)法動(dòng)態(tài)替換類代碼示例Native Image實(shí)踐下面是一個(gè)簡(jiǎn)單的Native Image示例展示構(gòu)建與運(yùn)行。// 適用 JDK 17 GraalVMimportjava.lang.management.ManagementFactory;importjava.lang.management.RuntimeMXBean;publicclassNativeDemo{publicstaticvoidmain(String[]args){longstartSystem.nanoTime();System.out.println(Hello from Native Image!);RuntimeMXBeanrbManagementFactory.getRuntimeMXBean();System.out.println(JVM uptime: rb.getUptime()ms);System.out.println(Elapsed: (System.nanoTime()-start)/1_000_000ms);}}構(gòu)建流程# 1. 設(shè)置GraalVM環(huán)境exportGRAALVM_HOME/path/to/graalvmexportJAVA_HOME$GRAALVM_HOME# 2. 編譯為字節(jié)碼javac NativeDemo.java# 3. AOT編譯為本地可執(zhí)行文件native-image NativeDemo# 4. 運(yùn)行./nativedemo典型對(duì)比同一程序運(yùn)行方式啟動(dòng)時(shí)間內(nèi)存占用可執(zhí)行文件大小java NativeDemo80ms30MB1KB(class)./nativedemo3ms8MB8MB(含運(yùn)行時(shí))這個(gè)差距在大型應(yīng)用上會(huì)更明顯——Spring Boot應(yīng)用的JVM啟動(dòng)可能5秒Native Image可能0.1秒。實(shí)踐要點(diǎn)先評(píng)估場(chǎng)景再選AOTAOT不是銀彈它的優(yōu)勢(shì)集中在啟動(dòng)敏感短生命周期。長(zhǎng)跑服務(wù)用標(biāo)準(zhǔn)JVMC2仍是最佳選擇。盲目追求Native Image可能犧牲峰值性能。反射配置要完整漏掉一個(gè)反射訪問(wèn)的類運(yùn)行時(shí)直接崩潰。用native-image-agent在測(cè)試環(huán)境跑一遍典型路徑收集配置再補(bǔ)充邊界場(chǎng)景。測(cè)試覆蓋率越高AOT遷移越穩(wěn)。第三方庫(kù)的兼容性檢查依賴庫(kù)是否聲明支持Native Image通常通過(guò)META-INF/native-image/目錄提供配置。Spring生態(tài)主流庫(kù)已支持但冷門(mén)庫(kù)可能不兼容需自行提供配置或?qū)ふ姨娲?gòu)建復(fù)雜度上升Native Image構(gòu)建比java -jar復(fù)雜得多CI/CD流水線要適配。構(gòu)建時(shí)間可能從幾十秒漲到幾分鐘且需要GraalVM環(huán)境。調(diào)試體驗(yàn)變差Native Image的棧跟蹤與JVM不同部分調(diào)試工具不適用。錯(cuò)誤信息可能不如JVM清晰。開(kāi)發(fā)期用JVM發(fā)布期用Native Image是主流的雙模式工作流。峰值性能要壓測(cè)AOT代碼無(wú)speculative optimization某些場(chǎng)景吞吐可能比JVM低20%~40%。上線前務(wù)必做完整壓測(cè)確認(rèn)峰值滿足SLA。若峰值不達(dá)標(biāo)考慮回歸JVM模式或用Profile-Guided OptimizationPGO先收集運(yùn)行時(shí)profile再用profile指導(dǎo)AOT編譯部分彌補(bǔ)無(wú)speculative的缺陷。GraalVM版本與JDK對(duì)齊GraalVM的JDK版本基于OpenJDK但有滯后。確認(rèn)GraalVM支持的JDK特性與你的代碼兼容如records、sealed classes、pattern matching等新特性的支持時(shí)機(jī)。分層策略對(duì)啟動(dòng)敏感的入口服務(wù)用Native Image對(duì)內(nèi)部長(zhǎng)跑服務(wù)用標(biāo)準(zhǔn)JVM?;旌霞軜?gòu)能兼顧啟動(dòng)與峰值是云原生時(shí)代的主流選型。監(jiān)控Native Image應(yīng)用Native Image支持JFRJava Flight Recorder和JMX但部分指標(biāo)與JVM不同。監(jiān)控方案要調(diào)整重點(diǎn)關(guān)注內(nèi)存、GC、啟動(dòng)時(shí)間。Heap dump格式也與標(biāo)準(zhǔn)JVM有差異分析工具要適配。小結(jié)Graal是用Java編寫(xiě)的現(xiàn)代JIT編譯器可作為C2的實(shí)驗(yàn)性替代也是GraalVM生態(tài)的底座jaotc是JDK 9~17的AOT工具因保守優(yōu)化與維護(hù)成本JDK 18起移除已被Native Image路線取代GraalVM Native Image通過(guò)閉世界分析把整個(gè)應(yīng)用AOT編譯為本地可執(zhí)行文件啟動(dòng)快、內(nèi)存少但喪失Java的動(dòng)態(tài)性AOT的代價(jià)是反射、動(dòng)態(tài)代理、運(yùn)行時(shí)類加載等動(dòng)態(tài)特性受限需通過(guò)配置文件提前聲明native-image-agent是收集配置的主流手段Spring Native通過(guò)編譯時(shí)AOT處理讓Spring生態(tài)擁抱Native Image適合Serverless/CLI/微服務(wù)冷啟動(dòng)場(chǎng)景C1/C2/Graal/Native Image各有定位長(zhǎng)跑用C2、啟動(dòng)敏感用Native Image、Graal是未來(lái)方向——技術(shù)選型取決于場(chǎng)景沒(méi)有銀彈本模塊至此結(jié)束。從JIT的分工架構(gòu)到具體優(yōu)化手段再到AOT的前沿探索我們看到了HotSpot幾十年工程積累的全貌。下一篇將進(jìn)入**Java內(nèi)存模型JMM**模塊從硬件內(nèi)存層級(jí)開(kāi)始剖析volatile、happens-before、內(nèi)存屏障的底層原理。