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