Spring Boot3 新玩法,AOT 最佳化!

張哥說技術發表於2024-01-16

來源:江南一點雨


今天和小夥伴們來聊一聊 Spring6 中的一個新特性 AOT(Ahead of Time),這也是目前在學習 Spring6 原始碼影片的小夥伴的一個提問,其實還是挺有代表意義的,因此松哥整理一篇文章來和大家聊一聊這個話題。

1. JIT VS AOT

1.1 JIT

JIT 是即時編譯(Just-In-Time Compilation)的縮寫。它是一種在程式執行時將程式碼動態編譯成機器碼的技術。與傳統的靜態編譯(Ahead-of-Time Compilation)不同,靜態編譯是在程式執行之前將程式碼編譯成機器碼。

JIT 編譯器在程式執行時根據需要將程式碼片段編譯成機器碼,以提高程式的執行效率。JIT 編譯器通常用於解釋型語言或動態語言的執行環境中,可以在執行時將解釋的程式碼轉換為機器碼,從而提高程式的執行速度。

所以 JIT 啟動比較慢,因為編譯需要佔用執行時的資源。

1.2 AOT

AOT 是預先編譯(Ahead-of-Time Compilation)的縮寫。它是一種在程式執行之前將程式碼靜態編譯成機器碼的技術。與即時編譯(JIT)不同,即時編譯是在程式執行時動態地將程式碼編譯成機器碼。AOT 編譯器在程式構建或安裝階段將程式碼轉換為機器碼,然後在執行時直接執行機器碼,而無需再進行編譯過程。這種靜態編譯的方式可以提高程式的啟動速度和執行效率,但也會增加構建和安裝的時間和複雜性。AOT 編譯器通常用於靜態語言的編譯過程,如 C、C++ 等。

在 Spring 中應用 AOT 需要關注以下注意事項:

  1. 類路徑是固定的,並且在構建的時候定義好。
  2. 程式中定義的 Bean 不能在執行的時候修改,這意味著:
    1. @Profile 註解標記的環境需要在構建的時候就確定。
    2. 條件註解 @Conditional 中的限制條件僅在構建時候考慮。
  3. 透過 supplier 提供的 Bean 無法提前進行型別轉換(這種要在 Bean 建立的時候根據 supplier 去提供 Bean,具體可參考 Spring 原始碼影片)。
  4. 確保 Bean 的型別足夠精確。

2. AOT 工作流程

Spring Framework 6 引入了AOT(Ahead-Of-Time)編譯的概念,這是一種提前編譯 Spring 應用程式的技術,以最佳化執行時效能,減少啟動時間,併為建立 GraalVM 原生映象提供支援。

AOT 的工作原理是在應用程式打包過程中提前執行那些通常在執行時進行的操作。包括生成 Bean 定義、解析配置和處理依賴注入等。透過這種方式,Spring 應用程式可以在啟動時跳過這些步驟,從而加快啟動速度,並減少 JVM 在執行時的計算負擔。

AOT 的使用通常涉及以下幾個步驟:

  1. 使用 ApplicationContextAotGenerator:這是 AOT 引擎的入口點,它負責處理 ApplicationContext 的配置。它會建立 Bean 定義,但不會例項化 Bean。
  2. 重新整理 ApplicationContext:為了 AOT 處理,需要重新整理 ApplicationContext,但這個重新整理過程與傳統的不同,因為它不會建立 Bean 例項。
  3. 呼叫 BeanFactoryInitializationAotProcessor 實現:這些處理器會對 GenerationContext 進行操作,例如,生成程式碼來恢復 BeanFactory 的狀態。
  4. 更新 GenerationContext:完成上述步驟後,GenerationContext 會包含生成的程式碼、資源和類,這些都是應用程式執行所必需的。
  5. 生成 GraalVM 原生映象配置:使用 RuntimeHints 例項生成 GraalVM 原生映象配置檔案。
  6. 獲取 ApplicationContextInitializer 的類名:ApplicationContextAotGenerator#processAheadOfTime 會返回一個類名,這個類允許應用程式以 AOT 最佳化的方式啟動。

乍一看,AOT 不錯呀,還等什麼,趕緊用 AOT 來跑我的專案吧!

別急!首先大家看到了 AOT 的有點,但是,這些優點中也隱藏著一些問題:

  • 反射:反射允許程式碼在編譯時動態呼叫方法和訪問未知的欄位。AOT 編譯器無法確定動態呼叫的類和方法。
  • 屬性檔案:屬性檔案的內容可以在執行時更改。由於作用時機的問題,AOT 編譯器無法確定動態使用的屬性檔案。
  • 代理:代理可將方法呼叫動態重定向到其他物件,所以它會使 AOT 編譯器難以確定在執行時呼叫哪些類和方法。
  • 序列化:序列化將物件的狀態轉換為位元組流,反之亦然,這會使 AOT 編譯器難以確定將在執行時呼叫哪些類和方法。

不過對於這些問題其實也都有辦法處理,這就是 AOT 預處理了,這個我們們後文說。

3. 實踐

接下來我們就來透過一個案例體驗下 AOT 具體應用吧。

3.1 準備工作

Java 虛擬機器通常是 JIT 形式,如果我們想要體驗 AOT,那麼就需要一個既支援 JIT 又支援 AOT 的工具了,這就是 GraalVM。

GraalVM 是一種高效能的通用虛擬機器,它為 Java 應用提供 AOT 編譯和二進位制打包能力,基於 GraalVM 打出的二進位制包可以實現快速啟動、具有超高效能、無需預熱時間、同時需要非常少的資源消耗。

GraalVM 非常有特色的一個功能是提供了 Native Image 打包技術,這種打包方式可以將應用程式打包為一個可脫離 JVM 獨立執行的二進位制包,這樣就省去了 JVM 載入和位元組碼執行期預熱的時間,提升了程式的執行效率。

和我們常用的 HotSpot JVM 相比主要有如下區別:

  1. 編譯器技術:HotSpot JVM 使用傳統的即時編譯器(JIT)技術,將位元組碼實時編譯為本地機器碼。而 GraalVM 使用了一種新的即時編譯器技術,稱為 Graal 編譯器。Graal 編譯器採用了基於圖形的最佳化方法,可以更好地最佳化程式碼並提高執行效能。
  2. 多語言支援:HotSpot JVM 主要為 Java 語言提供執行時環境,而 GraalVM 支援多種程式語言,包括 Java、JavaScript、Python、Ruby 等。這使得 GraalVM 成為一個更加通用和靈活的虛擬機器。
  3. 記憶體佔用:GraalVM 在記憶體佔用方面相對較低,這是由於其編譯器技術和最佳化策略的改進。相比之下,HotSpot JVM 在某些情況下可能會佔用更多的記憶體。
  4. 生態系統整合:HotSpot JVM 是 Java 開發生態系統中廣泛使用的虛擬機器,有大量的工具和框架與其整合。GraalVM 也可以與現有的 Java 生態系統整合,但由於其多語言支援和特殊的編譯器技術,可能需要一些額外的配置和適配。

當然,更重要的是,GraalVM 既支援 JIT 又支援 AOT。

所以,我們需要首先下載並安裝 GraalVM。

下載地址:,大家下載和自己 JDK 版本對應的 GraalVM。

這個下載之後直接解壓就可以了,解壓之後,將 GraalVM 配置到環境變數中就可以了。

最後,還需要安裝一下 native-image,當然大家可以順便用這個安裝檢驗一下自己的 GraalVM 是否配置正確:

Spring Boot3 新玩法,AOT 最佳化!

3.2 程式碼實踐

接下來我們建立一個 Spring Boot 工程,來體驗一下 AOT 提前編譯。

首先在建立工程的時候我們多新增一個依賴 GraalVM Native Support,如下圖:

Spring Boot3 新玩法,AOT 最佳化!

這是一個用來支援 AOT 的外掛。

程式碼建立好之後,我們隨便開發一個 /hello 介面,然後就來給專案打包。

3.2.1 傳統打包

直接點選 package 進行打包:

Spring Boot3 新玩法,AOT 最佳化!

打包結果:

Spring Boot3 新玩法,AOT 最佳化!

這個就是我們傳統的打包方式,沒啥好說的。大家注意一下這種傳統打包方式打包的時間是 4.86s。

3.2.2 native image 打包

接下來我們來看下 native image 打包。

執行如下命令進行 native image 打包:

mvn clean native:compile -Pnative

打包結果如下圖:

Spring Boot3 新玩法,AOT 最佳化!

大家看這個構建時間超級長。

再來看 native image 構建的結果:

Spring Boot3 新玩法,AOT 最佳化!

大家看到,除了我們所熟悉的 xxx.jar,還有一個可執行檔案。

因為我這裡是 Mac,所以打包出來的可執行檔案沒有字尾,如果在 Windows 上測試的話,打包出來的就是 aot_demo.exe 了。

現在這兩個都可以直接執行。

jar 包就不用說了,大家都比較熟悉了。aot_demo 這個檔案則是一個可以脫離 JVM 直接執行的二進位制檔案,啟動效率會高很多。

根據第二小節的介紹,我們知道在打成原生包的時候,Spring AOT 會先進行 AOT 預處理,這個處理過程會建立 Bean 的定義,但是不會例項化 Bean,我們可以分析一下編譯的結果就知道了。

首先我的原始碼,除了啟動類有兩個類,分別是:

@RestController
public class HelloController {

    @Autowired
    HelloService helloService;

    @GetMapping("/hello")
    public String hello() {
        return helloService.sayHello();
    }
}
@Service
public class HelloService {
    public String sayHello() {
        return "hello aot";
    }
}

在打 native-image 的時候,我們看下結果:

Spring Boot3 新玩法,AOT 最佳化!Spring Boot3 新玩法,AOT 最佳化!Spring Boot3 新玩法,AOT 最佳化!

看過鬆哥之前將的 Spring 原始碼分析的小夥伴,這塊的程式碼應該都很好明白,這就是直接把 BeanDefinition 給解析出來了,不僅註冊了當前 Bean,也把當前 Bean 所需要的依賴給注入了,將來 Spring 執行的時候就不用再去解析 BeanDefinition 了。

同時我們可以看到在 META-INF 中生成了 reflect、resource 等配置檔案。這些是我們新增的 native-maven-plugin 外掛所分析出來的反射以及資源資訊,將自動將這些作為配置檔案生成的。

這塊其實能聊的還蠻多,而且作為一個新支援的特性,Spring 對其功能也在不斷完善,松哥後面會繼續跟大家捋一捋這塊的內容。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024923/viewspace-3003963/,如需轉載,請註明出處,否則將追究法律責任。

相關文章