GraalVM 背景
新、舊程式語言的興起躁動,說明必然有其需求動力所在,譬如網際網路之於JavaScript、人工智慧之於Python,微服務風潮之於Golang等等。大家都清楚不太可能有哪門語言能在每一個領域都盡佔優勢,Java已是距離這個目標最接近的選項,但若“天下第一”還要百尺竿頭更進一步的話,似乎就只能忘掉Java語言本身,踏入無招勝有招的境界。
- 更進一步提升JVM上執行的程式的效能
- 通過預編譯(ahead-of-time)編譯Java程式為原生可執行程式
- 多種程式語言混編在一個程式中(polyglot)
- 類似於LLVM,GraalVM也提供了方便的機制方便開發新的程式語言
官方網站在: https://www.graalvm.org/
當前痛點
在雲原生時代,Java程式是有很大的劣勢的,為什麼這麼說呢?一般的Java應用程式都要幾十兆的記憶體,啟動也不不快。
最流行的SpringBoot/SpringCloud微服務框架為例,啟動一個已經優化好,很多bean需要lazy load的application至少需要3-4秒時間,記憶體需要幾百兆,業務邏輯稍微複雜一點點,沒有1G以上的記憶體是很難滿足業務的需要呢?
那麼在雲原生時代,一個充滿黑科技的JVM介紹給大家,它能幫助我們讓Java程式的啟動速度加快100倍,記憶體只需要原來的五分之一,甚至更少。
Graalvm的介紹
-
GraalVM是2018年Oracle開發的下一代JVM實現,被官方稱為“Universal VM”和“Polyglot VM”,這是一個在HotSpot虛擬機器基礎上增強而成的跨語言全棧虛擬機器,可以作為“任何語言”的執行平臺使用。
-
這裡“任何語言”包括了Java、Scala、Groovy、Kotlin等基於Java虛擬機器之上的語言,還包括了C、C++、Rust等基於LLVM的語言,同時支援其他像JavaScript、Ruby、Python和R語言等等。
GraalVM可以無額外開銷地混合使用這些程式語言,支援不同語言中混用對方的介面和對
象,也能夠支援這些語言使用已經編寫好的本地庫檔案。
它的口號“Run Programs Faster Anywhere”就能感覺到一顆蓬勃的野心
Graalvm效能的對比
GraalVM的效能真的不錯。以JDK 8為例
- OpenJDK
- Oracle JDK
其中OpenJDK是通過“GPL v2 with CE”協議開源的,可以免費商用的。
之前在用Apache Spark測試效能時, 對比一下兩者效能, 稍微資料量大點的查詢,會發現Oracle JDK一般都會比OpenJDK快30%以上。
-
而GraalVM分為社群版和商業版,其中GraalVM的社群版也是採用了和OpenJDK一樣的“GPL v2 with CE”協議開源的。
-
對於GraalVM的社群版,非常驚喜的發現其比Oracle JDK也會快10%以上。
-
沒有試過GraalVM的商業版, 官方報導,其商業版比社群版提升的效能更多
Graalvm主要特性
- 高效能的現代Java
- 佔用資源少,啟動速度快
- JavaScript,Java, Ruby以及R混合程式設計
- 在JVM上執行原生語言
- 跨語言工具
- JVM應用擴充套件
- 原生應用擴充套件
- 本地Java庫
- 資料庫支援多語言
- 建立自己的語言
Graalvm工作原理
GraalVM的基本工作原理是將這些語言的原始碼(例如,JavaScript)或原始碼編譯後的中間格式(例如,LLVM位元組碼、Class位元組碼)通過直譯器轉換為能被GraalVM接受的中間表示(Intermediate Representation,IR),譬如設計一個直譯器專門對LLVM輸出的位元組碼進行轉換來支援C和C++語言,這個過程稱為“程式特化”(Specialized,也常稱為Partial Evaluation)。
GraalVM提供了Truffle工具集來快速構建面向一種新語言的直譯器,並用它構建了一個稱為Sulong的高效能LLVM位元組碼直譯器。
從某個角度來看,GraalVM才是真正意義上與物理計算機相對應的高階語言虛擬機器,因為它與物理硬體的指令集一樣,做到了只與機器特性相關而不與某種高階語言特性相關。
Graalvm的高等優化能力
Oracle Labs的研究總監Thomas Wuerthinger在接受採訪時談到:“隨著GraalVM1.0的釋出,已經證明了擁有高效能的多語言虛擬機器是可能的,並且實現這個目標的最佳方式不是通過類似Java虛擬機器和微軟CLR那樣帶有語言特性的位元組碼”。
本來就不以速度見長的語言執行環境,由於GraalVM本身能夠對輸入的中間表示進行自動優化,在執行時還能進行即時編譯優化,往往使用GraalVM實現能夠獲得比原生編譯器更優秀的執行效率,譬如Graal.js要優於Node.js、Graal.Python要優於CPtyhon,TruffleRuby要優於Ruby MRI,FastR要優於R語言等等。
Graalvm與Hotspot的對比
GraalVM本來就是在HotSpot基礎上誕生的,天生就可作為一套完整的符合Java SE8標準Java虛擬機器來使用。
它和標準的HotSpot差異主要在即時編譯器上,其執行效率、編譯質量目前與標準版的HotSpot相比也是互有勝負。
Oracle Labs和美國大學裡面的研究院所做的最新即時編譯技術的研究全部都遷移至基於GraalVM之上進行了,其發展潛力令人期待。
如果Java語言或者HotSpot虛擬機器真的有被取代的一天,那從現在看來GraalVM是希望最大的一個候選項,這場革命很可能會在Java使用者沒有明顯感覺的情況下悄然而來,Java世界所有的軟體生態都沒有發生絲毫變化,但天下第一的位置已經悄然更迭。
Graalvm即時編譯器
自JDK 10起,HotSpot中又加入了一個全新的即時編譯器:Graal編譯器,看名字就可以聯想到它是來自於Graal VM。
C1/C2即時編譯器
對需要長時間執行的應用來說,由於經過充分預熱,熱點程式碼會被HotSpot的探測機制準確定位捕獲,並將其編譯為物理硬體可直接執行的機器碼,在這類應用中Java的執行效率很大程度上是取決於即時編譯器所輸出的程式碼質量。
HotSpot虛擬機器中包含有兩個即時編譯器:
- 編譯時間較短但輸出程式碼優化程度較低的客戶端編譯器(簡稱為C1)
- 編譯耗時長但輸出程式碼優化質量也更高的服務端編譯器(簡稱為C2)
通常它們會在分層編譯機制下與直譯器互相配合來共同構成HotSpot虛擬機器的執行子系統的。
C2即時編譯器
Graal編譯器是作為C2編譯器替代者的角色登場的。C2的歷史已經非常長了,可以追溯到Cliff Click大神讀博士期間的作品,這個由C++寫成的編譯器儘管目前依然效果拔群,但已經複雜到連Cliff Click本人都不願意繼續維護的程度。
Graal編譯器
而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注入更快更強的驅動力。
編譯為原生執行程式
編譯為原生程式有一定的假設條件,比如:
- 儘量少的JNI呼叫
- 儘量少的使用反射
- 儘量少的class loader隔離等
當沒有用這些複雜功能的時候,很容易可以使用GraalVM提供的 native image 編譯Jar為可執行程式。
當然即使當程式使用了 JNI、反射時,也沒關係,我們可以使用一些配置檔案告訴GraalVM單獨處理這些資訊,比如:
- 通過引數 -H:JNIConfigurationFiles 告訴JNI相關配置JSON檔案
- 通過引數 -H:ReflectionConfigurationFiles 告訴反射相關配置JSON檔案
稍微會複雜一些,但是隻要有足夠的耐心,理論上也是可以編譯成功的!
不過我們可以使用一些原生支援GraalVM native image的框架, 比如:Quarkus。
GraalVM的原生編譯非常適合微服務和 Serverless
當可以把Java程式也編譯為原生的可執行程式後 (目前GraalVM已經支援編譯為Windows, MacOS, Linux上的原生程式),最主要的兩個變化:
-
啟動時間變短了,之前啟動一個有“依賴注入”的Java程式,可能啟動時間要2秒以上。如果Java程式是要長期執行的,那啟動時間稍慢一點是沒問題的,但是對於 Serverless 應用,這就變為冷啟動(cold start)了,影響比較大。
-
程式執行的記憶體需求變小了,之前啟動一個Java程式,控制的好的話(heap設定的比較小),也要100M以上的記憶體,但是編譯為原生程式後,只需要4M記憶體就可以了。 這樣同樣的一臺機器就可以啟動非常多的程式,適合簡單的微服務。