傳統的一個 Java 應用從代碼編寫到啟動運行大致可以分為如下步驟:
整個過程如下圖所示:
圖 1:Java 程序運行過程
上述過程既給 Java 程序帶來了其他編程語言不具備的優勢,比如跨平臺,易上手等。但同時也給 Java 程序帶來了一些性能問題,比如啟動速度慢和運行時內存占用高等。
圖 1 中 Java 程序啟動運行詳細過程如下圖 2 所示:
圖 2:Java 程序的啟動過程分析[1]
一個 Java 應用啟動過程首先需要加載該應用程序對應的 JVM 虛擬機軟件程序到內存中,如上圖紅色部分描述所示。然后 JVM 虛擬機再加載對應的應用程序到內存中,該過程對應上圖中的淺藍色類加載(Class Load,CL)部分。在類加載過程中,應用程序就會開始被解釋執行,對應上圖中淺綠色部分。解釋執行過程 JVM 對垃圾對象進行回收,對應上圖中的黃色部分。隨著程序的運行的深入,JVM 會采用及時編譯(Just In Time,JIT)技術對執行頻率較高的代碼進行編譯優化,以便提升應用程序運行速度。JIT 過程對應上圖中的白色部分。經過 JIT 編譯優化后的代碼對應圖中深綠色部分。
經過上述分析,不難看出,一個 Java 程序從啟動到達到被JIT動態編譯優化會經過 VM init,App init 和 App active 幾個階段,相比于其他一些編譯型語言,其冷啟動問題比較嚴重。
除了冷啟動問題,從上述分析中不難看出,一個 Java 程序運行過程中,什么都不做首先就需要加載一個 JVM 虛擬機,該操作一般占用一定內存。另外,由于 Java 程序是先解釋執行字節碼,然后再做 JIT 編譯優化。
由于相比于一些編譯型語言其將編譯優化的動作后置到運行時,因此非常容易出現實際加載的代碼比實際需要運行的代碼多很多的情況,造成了一些無效內存占用情況。綜上所述就是為什么很多人常詬病 Java 程序運行內存占用高的幾點主要原因。
既然,先解釋執行再動態編譯的 Java 傳統程序運行方式存在上述諸多問題,那有沒有一些方式可以讓 Java 程序也跟其他程序語言,比如 C/C++ 一樣,先編譯后執行解決上述問題呢?
答案是肯定的,提前編譯(Ahead-of-Time Compilation,AOT Compilation)或者叫靜態編譯在 Java 領域很早就被提了出來。其核心思想就是將 Java 程序的編譯階段提前到程序啟動前,然后在編譯階段進行代碼編譯優化,讓程序啟動既巔峰,消除冷啟動,降低運行時內存開銷。
Java 領域靜態編譯的實現技術有很多,其中最具代表性的還屬 Oracle 推出的 GraalVM 開源高性能多語言運行時平臺[2]??吹竭@里有的讀者可能會問:“高性能多語言運行時平臺是什么?它跟靜態編譯本身有什么關系?”。
圖 3:GraalVM 多語言運行時平臺
如上圖 3 所示,GraalVM 中通過提供 Truffle 解釋器實現框架,讓開發人員可以使用 Truffle 提供的 API 快速實現特定語言的解釋器從而實現對上圖中各種編程語言所寫的程序都能進行編譯運行的效果,從而成為一個多語言運行時平臺。GraalVM 實現靜態編譯能力的編譯器就是 GraalVM JIT Compiler。靜態編譯框架和運行時由 Substrate VM 子項目實現,兼容 OpenJDK 運行時實現,提供了原生鏡像程序運行時的異常處理、同步調度、線程管理、內存管理等功能。
因此,GraalVM 不僅可以作為一個多語言運行時平臺,而且由于其中提供的 GraalVM JIT Compiler 靜態編譯器,其可用來對 Java 程序進行靜態編譯。
說完靜態編譯和 GraalVM 之間的關系,有的讀者可能會好奇,基于 GraalVM 的靜態編譯與常規的 JVM 解釋執行方式有哪些區別?基于靜態編譯的 Java 程序相比于目前應用廣泛的 JVM 運行時編譯 Java 程序整個從代碼編寫到編譯執行的區別如下圖 4 所示:
圖 4:靜態編譯與傳統 JVM 運行過程對比
相比于 JVM 運行時方式,靜態編譯在運行之前會先對程序解析編譯,然后生成一個跟運行時環境強相關的 native image 可執行文件,最后直接執行該文件即可啟動程序進行執行。
說到這里可能有的讀者又好奇,上圖 4 中的靜態編譯過程到底會對 Java 程序做哪些解析操作?靜態編譯后的可執行程序垃圾回收問題怎么解決?如下圖 5 所示,其描述了 GraalVM 靜態編譯技術實現中編譯過程的輸入與輸出內容。
圖 5:靜態編譯輸入輸出
圖 5 中左側前三個輸入內容 Applicaton,Libraries 和 JDK 是一個 Java 程序編譯運行必備的三部分,不必多說。而 Substrate VM 就是 GraalVM 中實現靜態編譯的核心部分,在整個靜態編譯過程中扮演了重要作用。
其中在靜態分析過程中,如上圖 5 中間部分中所繪制,Substrate VM 通過上下文不敏感的指向分析(Points-to Analysis)來對應用程序做靜態分析,其可以在不需要運行程序的情況下,基于源程序分析給出所有可能的可達函數列表然后作為后續編譯階段的輸入對程序進行靜態編譯。該過程由于靜態分析的局限性,無法覆蓋 Java 中的反射、動態代理、JNI 調用等動態特性。這也造成了很多的 Java 框架由于在實現過程中使用了大量的上述特性,因此,都難以直接基于 Substrate VM 完成對自身所有代碼的靜態分析,需要通過額外的外部配置[3]來解決靜態分析本身的不足。
例如像 Spring 社區因此開發了 AOT Engine[4]如下圖 6 所示來幫助解決 Spring 項目對其中的反射,動態代理等內容進行靜態分析處理并將其轉換為 Substrate VM 能在編譯階段可識別的內容,確保對 Spring 應用可基于 Substrate VM 順利完成靜態編譯。
圖 6:Spring AOT Engine
在靜態分析完成后,基于靜態分析結果的可達函數列表,調用上文介紹的 GraalVM 中的 GraalVM JIT Compiler 編譯器將應用程序編譯為與目標平臺強相關的本地代碼以完成編譯過程。
編譯完成后,就會進入到上圖 5 右側 Native 可執行文件生成階段。在該過程中,Substrate VM 會將靜態編譯階段確定和初始化的內容以及跟 Substrate VM 運行時以及 JDK 庫中的數據一起保存到最終可執行文件的 Image Heap 中。其中 Substrate VM 運行時就為最終可執行文件提供了運行過程中所需的垃圾回收、異常處理等能力。對于垃圾回收這塊,在一開始的 GraalVM 社區版中僅提供了 Serial GC。企業版中提供了能力更強的 G1 GC。不過在最新的社區版中 GraalVM 團隊也引入了 G1 GC[5]以便為廣大開發者提供更強大的靜態編譯使用能力。
上節,簡單介紹了靜態編譯技術以及其本身的局限性以后,很多外部社區開發者這時可能會疑問,一個 Java 開源項目如何快速進行靜態編譯適配?對于這個問題,其實最核心要解決的本質問題就是將開源框架中的 GraalVM 無法識別和處理的動態內容轉換為其可識別的內容即可。因此該問題由于不同框架情況不一樣,因此解決方式也會有一些差異。例如在 Spring 中,針對其自身框架開發的 AOT Engine 可以解決其框架提供的通過 @Configuration 注解注冊類初始化過程無法在靜態編譯階段被識別、提前在靜態編譯期生成原本在運行階段才能生成的動態代理類解決直接靜態編譯代理類無法被有效生成等問題[6]從而實現 Spring 應用的靜態編譯適配。
對于很多基于 Spring 實現的開源框架,如果本身無法被 GraalVM 識別的動態特性都是由于 Spring 標準的那一套用法所導致,由于自身屬于 Spring 體系,靜態編譯過程就肯定少不了 Spring AOT Engine 的參與,因此,框架自身就不需要再提供任何適配就可以具備靜態編譯能力。
對于非 Spring 體系項目或者自身使用了一些 JDK 中原生的反射或者其他 Java 動態特性,針對自身代碼中的 Java 動態用法需要在項目中提供對應的靜態配置文件才能在靜態編譯過程中讓編譯器識別其中的動態特性,對其進行編譯構建才能實現項目的順利編譯與執行。針對這種情況,GraalVM 提供了一個名叫 native-image-agent 的 Tracing Agent 來幫助大家更方便地收集元數據并準備配置文件。該 Agent 會在常規 Java VM 上的應用程序運行過程中自動收集其中的動態特性使用情況并將其轉換為 GraalVM 可以識別的配置文件。最后,將通過 Agent 生成的框架自身的動態配置文件存放在項目的:META-INF/native-image/<group.id>/<artifact.id> 目錄下,就可以在靜態編譯過程中根據這些配置內容,識別項目包中的動態特性。
Spring Cloud Alibaba 2022.0.0.0 版本所包含的所有中間件客戶端目前已完成了構建 GraalVM 原生應用的適配。由于項目自身的特定,項目整體實現中有大量的 Spring 語法導致的無法被 GraalVM 識別的動態特性用法,這塊內容直接交由 Spring AOT Engine 來進行解決,社區未做額外適配工作。
除了 Spring 體系語法,項目本身還是有一些其他 Java 動態用法的,這塊社區通過 native-image-agent 來進行解析與動態配置生成。
Spring Cloud Alibaba 2022.0.0.0 版本所包含的所有中間件客戶端已完成了構建 GraalVM 原生應用的適配。為用戶提供了開箱即用的靜態編譯能力。相關功能體驗過程如下:
首先需要在首先在機器上安裝 GraalVM 發行版。您可以在 Liberica Native Image Kit 頁面上手動下載它,也可以使用像 SDKMAN! 這樣的下載管理器。本文演示環境為 MacOS,如果是 Windows 可參考相應文檔[7]進行操作。執行以下命令安裝 GraalVM 環境:
$ sdk install java 22.3.r17-nik$ sdk use java 22.3.r17-nik
通過檢查 java -version 的輸出來驗證是否配置了正確的版本:
$ java -versionopenjdk version "17.0.5" 2022-10-18 LTSOpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode)
要使用 GraalVM 靜態編譯能力構建微服務,首先確保您項目的 Spring Boot 版本為 3.0.0 或以上,Spring Cloud 版本為 2022.0.0 或以上。然后在項目中引入 Spring Cloud Alibaba 2022.0.0.0 版本的所需模塊依賴即可。
通過以下命令生成應用中反射、序列化和動態代理所需的 Hints 配置文件,前提是應用中引入了 spring-boot-starter-parent 父模塊:
$ mvn -Pnative spring-boot:run
之后應用會啟動,進行預執行,需要盡可能完整的測試一遍應用的所有功能,保證應用的大部分代碼都被測試用例覆蓋,該過程會基于 GraalVM 的 native-image-agent 收集程序中的動態特性,這樣可以確保完整生成應用運行過程中的所有必須的動態屬性。運行完所有測試用例后,我們發現 resource/META-INF/native-image 目錄下會生成以下一些 hints 文件:
注意事項:Spring Cloud Alibaba 2022.0.0.0 正式版本所有核心模塊都已經默認將自身組件相關動態特性所需的配置內容都包含在了依賴中,因此上述預執行過程主要為了掃描應用自身業務代碼以及其他第三方包中的動態特性,以便后續靜態編譯過程能順利進行,應用能正常啟動。
以上步驟一切準備就緒后,通過以下命令來構建原生鏡像:
$ mvn -Pnative native:compile
成功執行后,我們在 /target 目錄可以看到生成的可執行文件。
與普通可執行文件無異,通過 target/xxx 啟動應用, 可以觀察到類似如下的輸出:
2023-08-01T17:21:21.006+08:00 INFO 65431 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''2023-08-01T17:21:21.008+08:00 INFO 65431 --- [ main] c.a.cloud.imports.examples.Application : Started Application in 0.553 seconds (process running for 0.562)
采用 GraalVM 靜態編譯技術的新版本 Spring Cloud Alibaba 應用,所有核心能力在啟動速度和內存占用率方面如下表所示都有顯著改善。
圖片
說明:上述測試代碼樣例來自 Spring Cloud Alibaba 項目中的 examples 模塊,4c16g Mac 環境,每組數據測試 3 次取平均,具體數據因機器不同可能會有差異。
相關鏈接:
[1] Java 程序的啟動過程分析
https://shipilev/talks/j1-Oct2011-21682-benchmarking.pdf
[2] GraalVM 開源高性能多語言運行時平臺
https://www.oracle.com/java/graalvm/
[3] 外部配置
https://www.graalvm.org/latest/reference-manual/native-image/metadata/
[4] AOT Engine
https://spring.io/blog/2021/12/09/new-aot-engine-brings-spring-native-to-the-next-level
[5] G1 GC
https://medium.com/graalvm/a-new-graalvm-release-and-new-free-license-4aab483692f5
[6] 動態代理類等問題
https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.introducing-graalvm-native-images.understanding-aot-processing
[7] 相應文檔
https://medium.com/graalvm/using-graalvm-and-native-image-on-windows-10-9954dc071311
本文鏈接:http://www.tebozhan.com/showinfo-26-6164-0.html基于靜態編譯構建微服務應用
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 如何編寫技術文檔?
下一篇: 系統架構設計之數據同步策略