引言
網上每隔一段時間就能見到幾條“未來X語言將會取代JAVA”的新聞,此處“X”可以用Kotlin、Golang、Dart、JavaScript、Python……等各種編程語言來代入。這大概就是長期占據編程語言榜單第一位的煩惱,天下第一總避免不了挑戰者相伴。
如果Java有擬人化的思維,它應該從來沒有懼怕過被哪一門語言所取代,Java“天下第一”的底氣不在于語法多么先進好用,而是來自它龐大的用戶群和極其成熟的軟件生態,這在朝夕之間難以撼動。不過,既然有那么多新、舊編程語言的興起躁動,說明必然有其需求動力所在,譬如互聯網之于JavaScript、人工智能之于Python,微服務風潮之于Golang等等。大家都清楚不太可能有哪門語言能在每一個領域都盡占優勢,Java已是距離這個目標最接近的選項,但若“天下第一”還要百尺竿頭更進一步的話,似乎就只能忘掉Java語言本身,踏入無招勝有招的境界。
Graal VM的誕生
2018年4月,Oracle Labs新公開了一項黑科技:Graal VM,從它的口號“Run Programs Faster Anywhere”就能感覺到一顆蓬勃的野心,這句話顯然是與1995年Java剛誕生時的“Write Once,Run Anywhere”在遙相呼應。
Graal VM
Graal VM被官方稱為“Universal VM”和“Polyglot VM”,這是一個在HotSpot虛擬機基礎上增強而成的跨語言全棧虛擬機,可以作為“任何語言”的運行平臺使用,這里“任何語言”包括了Java、Scala、Groovy、Kotlin等基于Java虛擬機之上的語言,還包括了C、C++、Rust等基于LLVM的語言,同時支持其他像JavaScript、Ruby、Python和R語言等等。Graal VM可以無額外開銷地混合使用這些編程語言,支持不同語言中混用對方的接口和對象,也能夠支持這些語言使用已經編寫好的本地庫文件。
Graal VM的基本工作原理是將這些語言的源代碼(例如JavaScript)或源代碼編譯后的中間格式(例如LLVM字節碼)通過解釋器轉換為能被Graal VM接受的中間表示(Intermediate Representation,IR),譬如設計一個解釋器專門對LLVM輸出的字節碼進行轉換來支持C和C++語言,這個過程稱為“程序特化”(Specialized,也常稱為Partial Evaluation)。Graal VM提供了Truffle工具集來快速構建面向一種新語言的解釋器,并用它構建了一個稱為Sulong的高性能LLVM字節碼解釋器。
以更嚴格的角度來看,Graal VM才是真正意義上與物理計算機相對應的高級語言虛擬機,理由是它與物理硬件的指令集一樣,做到了只與機器特性相關而不與某種高級語言特性相關。Oracle Labs的研究總監Thomas Wuerthinger在接受InfoQ采訪時談到:“隨著Graal VM 1.0的發布,我們已經證明了擁有高性能的多語言虛擬機是可能的,并且實現這個目標的最佳方式不是通過類似Java虛擬機和微軟CLR那樣帶有語言特性的字節碼”。對于一些本來就不以速度見長的語言運行環境,由于Graal VM本身能夠對輸入的中間表示進行自動優化,在運行時還能進行即時編譯優化,往往使用Graal VM實現能夠獲得比原生編譯器更優秀的執行效率,譬如Graal.js要優于Node.js、Graal.Python要優于CPtyhon,TruffleRuby要優于Ruby MRI,FastR要優于R語言等等。
針對Java而言,Graal VM本來就是在HotSpot基礎上誕生的,天生就可作為一套完整的符合Java SE 8標準Java虛擬機來使用。它和標準的HotSpot差異主要在即時編譯器上,其執行效率、編譯質量目前與標準版的HotSpot相比也是互有勝負。但現在Oracle Labs和美國大學里面的研究院所做的最新即時編譯技術的研究全部都遷移至基于Graal VM之上進行了,其發展潛力令人期待。如果Java語言或者HotSpot虛擬機真的有被取代的一天,那從現在看來Graal VM是希望最大的一個候選項,這場革命很可能會在Java使用者沒有明顯感覺的情況下悄然而來,Java世界所有的軟件生態都沒有發生絲毫變化,但天下第一的位置已經悄然更迭。
新一代即時編譯器
對需要長時間運行的應用來說,由于經過充分預熱,熱點代碼會被HotSpot的探測機制準確定位捕獲,并將其編譯為物理硬件可直接執行的機器碼,在這類應用中Java的運行效率很大程度上是取決于即時編譯器所輸出的代碼質量。
HotSpot虛擬機中包含有兩個即時編譯器,分別是編譯時間較短但輸出代碼優化程度較低的客戶端編譯器(簡稱為C1)以及編譯耗時長但輸出代碼優化質量也更高的服務端編譯器(簡稱為C2),通常它們會在分層編譯機制下與解釋器互相配合來共同構成HotSpot虛擬機的執行子系統的。
自JDK 10起,HotSpot中又加入了一個全新的即時編譯器:Graal編譯器,看名字就可以聯想到它是來自于前一節提到的Graal VM。Graal編譯器是作為C2編譯器替代者的角色登場的。C2的歷史已經非常長了,可以追溯到Cliff Click大神讀博士期間的作品,這個由C++寫成的編譯器盡管目前依然效果拔群,但已經復雜到連Cliff Click本人都不愿意繼續維護的程度。而Graal編譯器本身就是由Java語言寫成,實現時又刻意與C2采用了同一種名為“Sea-of-Nodes”的高級中間表示(High IR)形式,使其能夠更容易借鑒C2的優點。Graal編譯器比C2編譯器晚了足足二十年面世,有著極其充沛的后發優勢,在保持能輸出相近質量的編譯代碼的同時,開發效率和擴展性上都要顯著優于C2編譯器,這決定了C2編譯器中優秀的代碼優化技術可以輕易地移植到Graal編譯器上,但是反過來Graal編譯器中行之有效的優化在C2編譯器里實現起來則異常艱難。這種情況下,Graal的編譯效果短短幾年間迅速追平了C2,甚至某些測試項中開始逐漸反超C2編譯器。Graal能夠做比C2更加復雜的優化,如“部分逃逸分析”(Partial Escape Analysis),也擁有比C2更容易使用“激進預測性優化”(Aggressive Speculative Optimization)的策略,支持自定義的預測性假設等等。
今天的Graal編譯器尚且年幼,還未經過足夠多的實踐驗證,所以仍然帶著“實驗狀態”的標簽,需要用開關參數去激活,這讓筆者不禁聯想起JDK 1.3時代,HotSpot虛擬機剛剛橫空出世時的場景,同樣也是需要用開關激活,也是作為Classic虛擬機的替代品的一段歷史。
Graal編譯器未來的前途可期,作為Java虛擬機執行代碼的最新引擎,它的持續改進,會同時為HotSpot與Graal VM注入更快更強的驅動力。
向原生邁進
對不需要長時間運行的,或者小型化的應用而言,Java(而不是指Java ME)天生就帶有一些劣勢,這里并不光是指跑個HelloWorld也需要百多兆的JRE之類的問題,而更重要的是指近幾年從大型單體應用架構向小型微服務應用架構發展的技術潮流下,Java表現出來的不適應。
在微服務架構的視角下,應用拆分后,單個微服務很可能就不再需要再面對數十、數百GB乃至TB的內存,有了高可用的服務集群,也無須追求單個服務要7×24小時不可間斷地運行,它們隨時可以中斷和更新;但相應地,Java的啟動時間相對較長、需要預熱才能達到最高性能等特點就顯得相悖于這樣的應用場景。在無服務架構中,矛盾則可能會更加突出,比起服務,一個函數的規模通常會更小,執行時間會更短,當前最熱門的無服務運行環境AWS Lambda所允許的最長運行時間僅有15分鐘。
一直把軟件服務作為重點領域的Java自然不可能對此視而不見,在最新的幾個JDK版本的功能清單中,已經陸續推出了跨進程的、可以面向用戶程序的類型信息共享(Application Class Data Sharing,AppCDS,允許把加載解析后的類型信息緩存起來,從而提升下次啟動速度,原本CDS只支持Java標準庫,在JDK 10時的AppCDS開始支持用戶的程序代碼)、無操作的垃圾收集器(Epsilon,只做內存分配而不做回收的收集器,對于運行完就退出的應用十分合適)等改善措施。而醞釀中的一個更徹底的解決方案,是逐步開始對提前編譯(Ahead of Time Compilation,AOT)提供支持。
提前編譯是相對于即時編譯的概念,提前編譯能帶來的最大好處是Java虛擬機加載這些已經預編譯成二進制庫之后就能夠直接調用,而無須再等待即時編譯器在運行時將其編譯成二進制機器碼。理論上,提前編譯可以減少即時編譯帶來的預熱時間,減少Java應用長期給人帶來的“第一次運行慢”不良體驗,可以放心地進行很多全程序的分析行為,可以使用時間壓力更大的優化措施。
但是提前編譯的壞處也很明顯,它破壞了Java“一次編寫,到處運行”的承諾,必須為每個不同的硬件、操作系統去編譯對應的發行包。也顯著降低了Java鏈接過程的動態性,必須要求加載的代碼在編譯期就是全部已知的,而不能再是運行期才確定,否則就只能舍棄掉已經提前編譯好的版本,退回到原來的即時編譯執行狀態。
早在JDK 9時期,Java 就提供了實驗性的Jaotc命令來進行提前編譯,不過多數人試用過后都頗感失望,大家原本期望的是類似于Excelsior JET那樣的編譯過后能生成本地代碼完全脫離Java虛擬機運行的解決方案,但Jaotc其實僅僅是代替掉即時編譯的一部分作用而已,仍需要運行于HotSpot之上。
直到Substrate VM出現,才算是滿足了人們心中對Java提前編譯的全部期待。Substrate VM是在Graal VM 0.20版本里新出現的一個極小型的運行時環境,包括了獨立的異常處理、同步調度、線程管理、內存管理(垃圾收集)和JNI訪問等組件,目標是代替HotSpot用來支持提前編譯后的程序執行。它還包含了一個本地鏡像的構造器(Native Image Generator)用于為用戶程序建立基于Substrate VM的本地運行時鏡像。這個構造器采用指針分析(Points-To Analysis)技術,從用戶提供的程序入口出發,搜索所有可達的代碼。在搜索的同時,它還將執行初始化代碼,并在最終生成可執行文件時,將已初始化的堆保存至一個堆快照之中。這樣一來,Substrate VM就可以直接從目標程序開始運行,而無須重復進行Java虛擬機的初始化過程。但相應地,原理上也決定了Substrate VM必須要求目標程序是完全封閉的,即不能動態加載其他編譯期不可知的代碼和類庫。基于這個假設,Substrate VM才能探索整個編譯空間,并通過靜態分析推算出所有虛方法調用的目標方法。
Substrate VM帶來的好處是能顯著降低了內存占用及啟動時間,由于HotSpot本身就會有一定的內存消耗(通常約幾十MB),這對最低也從幾GB內存起步的大型單體應用來說并不算什么,但在微服務下就是一筆不可忽視的成本。根據Oracle官方給出的測試數據,運行在Substrate VM上的小規模應用,其內存占用和啟動時間與運行在HotSpot相比有了5倍到50倍的下降,具體結果如下圖所示:
啟動時間對比
啟動時間對比
Substrate VM補全了Graal VM“Run Programs Faster Anywhere”愿景藍圖里最后的一塊拼圖,讓Graal VM支持其他語言時不會有重量級的運行負擔。譬如運行JavaScript代碼,Node.js的V8引擎執行效率非常高,但即使是最簡單的HelloWorld,它也要使用約20MB的內存,而運行在Substrate VM上的Graal.js,跑一個HelloWorld則只需要4.2MB內存而已,且運行速度與V8持平。Substrate VM 的輕量特性,使得它十分適合于嵌入至其他系統之中,譬如Oracle自家的數據庫就已經開始使用這種方式支持用不同的語言代替PL/SQL來編寫存儲過程。
沒有虛擬機的Java
盡管Java已經看清楚了在微服務時代的前進目標,但是,Java語言和生態在微服務、微應用環境中的天生的劣勢并不會一蹴而就地被解決,通往這個目標的道路注定會充滿荊棘;盡管已經有了放棄“一次編寫,到處運行”、放棄語言動態性的思想準備,但是,這些特性并不單純是宣傳口號,它們在Java語言誕生之初就被植入到基因之中,當Graal VM試圖打破這些規則的同時,也受到了Java語言和在其之上的生態生態的強烈反噬,筆者選擇其中最主要的一些困難列舉如下:
-
某些Java語言的特性,使得Graal VM編譯本地鏡像的過程變得極為艱難。譬如常見的反射,除非使用安全管理器去專門進行認證許可,否則反射機制具有在運行期動態調用幾乎所有API接口的能力,且具體會調用哪些接口,在程序不會真正運行起來的編譯期是無法獲知的。反射顯然是Java不能放棄不能妥協的重要特性,為此,只能由程序的開發者明確地告知Graal VM有哪些代碼可能被反射調用(通過JSON配置文件的形式),Graal VM才能在編譯本地程序時將它們囊括進來。這是一種可操作性極其低下卻又無可奈何的解決方案,即使開發者接受不厭其煩地列舉出自己代碼中所用到的反射API,但他們又如何能保證程序所引用的其他類庫的反射行為都已全部被獲知,其中沒有任何遺漏?與此類似的還有另外一些語言特性,如動態代理等。另外,一切非代碼性質的資源,如最典型的配置文件等,也都必須明確加入配置中才能被Graal VM編譯打包。這導致了如果沒有專門的工具去協助,使用Graal VM編譯Java的遺留系統即使理論可行,實際操作也將是極度的繁瑣。
{ name: "com.github.fenixsoft.SomeClass",allDeclaredConstructors: true,allPublicMethods: true},{name: "com.github.fenixsoft.AnotherClass",fileds: [{name: "foo"}, {name: "bar"}],methods: [{name: "<init>",parameterTypes: ["char[]"]}]},// something else ……
-
大多數運行期對字節碼的生成和修改操作,在Graal VM看來都是無法接受的,因為Substrate VM里面不再包含即時編譯器和字節碼執行引擎,所以一切可能被運行的字節碼,都必須經過AOT編譯成為原生代碼。請不要覺得運行期直接生成字節碼會很罕見,誤以為導致的影響應該不算很大。事實上,多數實際用于生產的Java系統都或直接或講解、或多或少引用了ASM、CGLIB、Javassist這類字節碼庫。舉個例子,CGLIB是通過運行時產生字節碼(生成代理類的子類)來做動態代理的,長期以來這都是Java世界里進行類增強的主流形式,因為面向接口的增強可以使用JDK自帶的動態代理,但對類的增強則并沒有多少選擇的余地。CGLIB也是Spring用來做類增強的選擇,但Graal VM明確表示是不可能支持CGLIB的,因此,這點就必須由用戶(面向接口編程)、框架(Spring這些DI框架放棄CGLIB增強)和Graal VM(起碼得支持JDK的動態代理,留條活路可走)來共同解決。自Spring Framework 5.2起,@Configuration注解中加入了一個新的proxyBeanMethods參數,設置為false則可避免Spring對與非接口類型的Bean進行代理。同樣地,對應在Spring Boot 2.2中,@SpringBootApplication注解也增加了proxyBeanMethods參數,通常采用Graal VM去構建的Spring Boot本地應用都需要設置該參數。
-
一切HotSpot虛擬機本身的內部接口,譬如JVMTI、JVMCI等,在都將不復存在了——在本地鏡像中,連HotSpot本身都被消滅了,這些接口自然成了無根之木。這對使用者一側的最大影響是再也無法進行Java語言層次的遠程調試了,最多只能進行匯編層次的調試。在生產系統中一般也沒有人這樣做,開發環境就沒必要采用Graal VM編譯,這點的實際影響并不算大。
-
Graal VM放棄了一部分可以妥協的語言和平臺層面的特性,譬如Finalizer、安全管理器、InvokeDynamic指令和MethodHandles,等等,在Graal VM中都被聲明為不支持的,這些妥協的內容大多倒并非全然無法解決,主要是基于工作量性價比的原因。能夠被放棄的語言特性,說明確實是影響范圍非常小的,所以這個對使用者來說一般是可以接受的。
……
以上,是Graal VM在Java語言中面臨的部分困難,在整個Java的生態系統中,數量龐大的第三方庫才是真正最棘手的難題。可以預料,這些第三方庫一旦脫離了Java虛擬機,在原生環境中肯定會暴露出無數千奇百怪的異常行為。Graal VM團隊對此的態度非常務實,并沒有直接硬啃。要建設可持續、可維護的Graal VM,就不能為了兼容現有JVM生態,做出過多的會影響性能、優化空間和未來拓展的妥協犧牲,為此,應該也只能反過來由Java生態去適應Graal VM,這是Graal VM團隊明確傳遞出對第三方庫的態度:
3rd party libraries
Graal VM native support needs to be sustainable and maintainable, that's why we do not want to maintain fragile pathches for the whole JVM ecosystem. The ecosystem of libraries needs to support it natively.
—— Sébastien Deleuze,DEVOXX 2019
為了推進Java生態向Graal VM兼容,Graal VM主動拉攏了Java生態中最龐大的一個派系:Spring。從2018年起,來自Oracle的Graal VM團隊與來自Pivotal的Spring團隊已經緊密合作了很長的一段時間,共同創建了Spring Graal Native項目來解決Spring全家桶在Graal VM上的運行適配問題,在不久的將來(預計應該是2020年10月左右),下一個大的Spring版本(Spring Framework 5.3、Spring Boot 2.3)的其中一項主要改進就是能夠開箱即用地支持Graal VM,這樣,用于微服務環境的Spring Cloud便會獲得不受Java虛擬機束縛的更廣闊舞臺空間。
Spring over Graal
前面幾部分,我們以定性的角度分析了Graal VM誕生的背景與它的價值,在最后這部分,我們嘗試進行一些實踐和定量的討論,介紹具體如何使用Graal VM之余,也希望能以更加量化的角度去理解程序運行在Graal VM之上,會有哪些具體的收益和代價。
盡管需要到2020年10月正式發布之后,Spring對Graal VM的支持才會正式提供,但現在的我們其實已經可以使用Graal VM來(實驗性地)運行Spring、Spring Boot、Spring Data、Netty、JPA等等的一系列組件(不過SpringCloud中的組件暫時還不行)。接下來,我們將嘗試使用Graal VM來編譯一個標準的Spring Boot應用:
環境準備
-
安裝Graal VM,你可以選擇直接下載安裝(版本選擇Graal VM CE 20.0.0),然后配置好PATH和JAVA_HOME環境變量即可;也可以選擇使用SDKMAN來快速切換環境。個人推薦后者,畢竟目前還不適合長期基于Graal VM環境下工作,經常手工切換會很麻煩。
# 安裝SDKMAN $ curl -s "https://get.sdkman.io" | bash # 安裝Graal VM $ sdk install java 20.0.0.r8-grl
-
安裝本地鏡像編譯依賴的LLVM工具鏈。
# gu命令來源于Graal VM的bin目錄 $ gu install native-image
-
請注意,這里已經假設你機器上已有基礎的GCC編譯環境,即已安裝過build-essential、libz-dev等套件。沒有的話請先行安裝。對于windows環境來說,這步是需要Windows SDK 7.1中的C++編譯環境來支持。我個人并不建議在Windows上進行Java應用的本地化操作,如果說在linux中編譯一個本地鏡像,通常是為了打包到Docker,然后發布到服務器中使用。那在Windows上編譯一個本地鏡像,你打算用它來干什么呢?
編譯準備
-
首先,我們先假設你準備編譯的代碼是“符合要求”的,即沒有使用到Graal VM不支持的特性,譬如前面提到的Finalizer、CGLIB、InvokeDynamic這類功能。然后,由于我們用的是Graal VM的Java 8版本,也必須假設你編譯使用Java語言級別在Java 8以內。
-
然后,我們需要用到尚未正式對外發布的Spring Boot 2.3,目前最新的版本是Spring Boot 2.3.0.M4。請將你的pom.xml中的Spring Boot版本修改如下(假設你編譯用的是Maven,用Gradle的請自行調整):
<parent> <groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.0.M4</version><relativePath/></parent>
-
由于是未發布的Spring Boot版本,所以它在Maven的中央倉庫中是找不到的,需要手動加入Spring的私有倉庫,如下所示:
<repositories> <repository><id>spring-milestone</id><name>Spring milestone</name><url>https://repo.spring.io/milestone</url></repository></repositories>
-
最后,盡管我們可以通過命令行(使用native-image命令)來直接進行編譯,這對于沒有什么依賴的普通Jar包、寫一個Helloworld來說都是可行的,但對于Spring Boot,光是在命令行中寫Classpath上都忙活一陣的,建議還是使用Maven插件來驅動Graal VM編譯,這個插件能夠根據Maven的依賴信息自動組織好Classpath,你只需要填其他命令行參數就行了。因為并不是每次編譯都需要構建一次本地鏡像,為了不干擾使用普通Java虛擬機的編譯,建議在Maven中獨立建一個Profile來調用Graal VM插件,具體如下所示:
<profiles> <profile><id>graal</id><build><plugins><plugin><groupId>org.graalvm.nativeimage</groupId><artifactId>native-image-maven-plugin</artifactId><version>20.0.0</version><configuration><buildArgs>-Dspring.graal.remove-unused-autoconfig=true --no-fallback -H:+ReportExceptionStackTraces --no-server</buildArgs></configuration><executions><execution><goals><goal>native-image</goal></goals><phase>package</phase></execution></executions></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></profile></profiles>
-
這個插件同樣在Maven中央倉庫中不存在,所以也得加上前面Spring的私有庫:
<pluginRepositories> <pluginRepository><id>spring-milestone</id><name>Spring milestone</name><url>https://repo.spring.io/milestone</url></pluginRepository></pluginRepositories>
-
至此,編譯環境的準備順利完成。
程序調整
-
首先,前面提到了Graal VM不支持CGLIB,只能使用JDK動態代理,所以應當把Spring對普通類的Bean增強給關閉掉:
@SpringBootApplication(proxyBeanMethods = false) public class ExampleApplication {public static void main(String[] args) {SpringApplication.run(ExampleApplication.class, args);}}
-
然后,這是最麻煩的一個步驟,你程序里反射調用過哪些API、用到哪些資源、動態代理,還有哪些類型需要在編譯期初始化的,都必須使用JSON配置文件逐一告知Graal VM。前面也說過了,這事情只有理論上的可行性,實際做起來完全不可操作。Graal VM的開發團隊當然也清楚這一點,所以這個步驟實際的處理途徑有兩種,第一種是假設你依賴的第三方包,全部都在Jar包中內置了以上編譯所需的配置信息,這樣你只要提供你程序里用戶代碼中用到的配置即可,如果你程序里沒寫過反射、沒用過動態代理什么的,那就什么配置都無需提供。第二種途徑是Graal VM計劃提供一個Native Image Agent的代理,只要將它掛載在在程序中,以普通Java虛擬機運行一遍,把所有可能的代碼路徑都操作覆蓋到,這個Agent就能自動幫你根據程序實際運行情況來生成編譯所需要的配置,這樣無論是你自己的代碼還是第三方的代碼,都不需要做預先的配置。目前,第二種方式中的Agent尚未正式發布,只有方式一是可用的。幸好,Spring與Graal VM共同維護的在Spring Graal Native項目已經提供了大多數Spring Boot組件的配置信息(以及一些需要在代碼層面處理的Patch),我們只需要簡單依賴該工程即可。
<dependencies> <dependency><groupId>org.springframework.experimental</groupId><artifactId>spring-graal-native</artifactId><version>0.6.1.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-indexer</artifactId></dependency></dependencies>
-
另外還有一個小問題,由于目前Spring Boot嵌入的Tomcat中,WebSocket部分在JMX反射上還有一些瑕疵,在修正該問題的PR被Merge之前,暫時需要手工去除掉這個依賴:
<dependencies> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.Apache.tomcat.embed</groupId><artifactId>tomcat-embed-websocket</artifactId></exclusion></exclusions></dependency></dependencies>
-
最后,在Maven中給出程序的啟動類的路徑:
<properties> <start-class>com.example.ExampleApplication</start-class></properties>
開始編譯
-
到此一切準備就緒,通過Maven進行編譯:
$ mvn -Pgraal clean package
-
編譯的結果默認輸出在target目錄,以啟動類的名字命名。
-
因為AOT編譯可以放心大膽地進行大量全程序的重負載優化,所以無論是編譯時間還是空間占用都非常可觀。筆者在intel 9900K、64GB內存的機器上,編譯了一個只引用了org.springframework.boot:spring-boot-starter-web的Helloworld類型的工程,大約耗費了兩分鐘時間。
[com.example.exampleapplication:9839] (typeflow): 22,093.72 ms, 6.48 GB [com.example.exampleapplication:9839] (objects): 34,528.09 ms, 6.48 GB[com.example.exampleapplication:9839] (features): 6,488.74 ms, 6.48 GB[com.example.exampleapplication:9839] analysis: 65,465.65 ms, 6.48 GB[com.example.exampleapplication:9839] (clinit): 2,135.25 ms, 6.48 GB[com.example.exampleapplication:9839] universe: 4,449.61 ms, 6.48 GB[com.example.exampleapplication:9839] (parse): 2,161.78 ms, 6.32 GB[com.example.exampleapplication:9839] (inline): 3,113.77 ms, 6.25 GB[com.example.exampleapplication:9839] (compile): 15,892.88 ms, 6.56 GB[com.example.exampleapplication:9839] compile: 25,044.34 ms, 6.56 GB[com.example.exampleapplication:9839] image: 6,580.71 ms, 6.63 GB[com.example.exampleapplication:9839] write: 1,362.73 ms, 6.63 GB[com.example.exampleapplication:9839] [total]: 120,410.26 ms, 6.63 GB[INFO][INFO] --- spring-boot-maven-plugin:2.3.0.M4:repackage (repackage) @ exampleapplication ---[INFO] Replacing main artifact with repackaged archive[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 02:08 min [INFO] Finished at: 2020-04-25T22:18:14+08:00[INFO] Final Memory: 38M/599M[INFO] ------------------------------------------------------------------------
效果評估
-
筆者使用Graal VM編譯一個最簡單的Helloworld程序(就只在控制臺輸出個Helloworld,什么都不依賴),最終輸出的結果大約3.6MB,啟動時間能低至2ms左右。如果用這個程序去生成Docker鏡像(不基于任何基礎鏡像,即使用FROM scratch打包),產生的鏡像還不到3.8MB。而OpenJDK官方提供的Docker鏡像,即使是slim版,其大小也在200MB到300MB之間。
-
使用Graal VM編譯一個簡單的Spring Boot Web應用,僅導入Spring Boot的Web Starter的依賴的話,編譯結果有77MB,原始的Fat Jar包大約是16MB,這樣打包出來的Docker鏡像可以不依賴任何基礎鏡像,大小仍然是78MB左右(實際使用時最好至少也要基于alpine吧,不差那幾MB)。相比起空間上的收益,啟動時間上的改進是更主要的,Graal VM的本地鏡像啟動時間比起基于虛擬機的啟動時間有著絕對的優勢,一個普通Spring Boot的Web應用啟動一般2、3秒之間,而本地鏡像只要100毫秒左右即可完成啟動,這確實有了數量級的差距。
-
不過,必須客觀地說明一點,盡管Graal VM在啟動時間、空間占用、內存消耗等容器化環境中比較看重的方面確實比HotSpot有明顯的改進,盡管Graal VM可以放心大膽地使用重負載的優化手段,但如果是處于長時間運行這個前提下,至少到目前為止,沒有任何跡象表明它能夠超越經過充分預熱后的HotSpot。在延遲、吞吐量、可監控性等方面,仍然是HotSpot占據較大優勢,下圖引用了DEVOXX 2019中Graal VM團隊自己給出的Graal VM與HotSpot JIT在各個方面的對比評估:
Graal VM與HotSpot的對比
Graal VM團隊同時也說了,Graal VM有望在2020年之內,在延遲和吞吐量這些關鍵指標上追評HotSpot現在的表現。Graal VM畢竟是一個2018年才正式公布的新生事物,我們能看到它這兩三年間在可用性、易用性和性能上持續地改進,Graal VM有望成為Java在微服務時代里的最重要的基礎設施變革者,這項改進的結果如何,甚至可能與Java的前途命運息息相關。
作者簡介
周志明,騰訊云最具價值專家(TVP),Java技術、機器學習和企業級開發技術專家,現任遠光軟件研究院院長,機器學習方向博士, 開源技術的積極倡導者和推動者,對計算機科學和相關的多個領域都有深刻的見解,尤其是人工智能、Java技術和敏捷開發等領域。曾受邀在InfoQ和IBMDeveloperWorks等網站撰寫技術專欄。
著有暢銷書多本。著有《智慧的疆界》、《深入理解Java虛擬機》、《深入理解OSGi》,翻譯了《Java虛擬機規范》等著作。其中《深入理解Java虛擬機》第1版出版于2011年,已經出至第3版,累計印刷超過35次,銷量30萬冊;不僅銷量好,而且口碑更好,是中文計算機圖書領域公認的、難得一見的佳作。
本文首發于騰訊TVP公眾號,技術原創及架構實踐文章,歡迎通過公眾號菜單「聯系我們」進行投稿。
高可用架構
改變互聯網的構建方式