上週去聽了QCon全球開發大會,其中有幾場印象比較深刻的分享,除去幾個比較概念化的話題,在Java技術演進這個Topic裡的幾個分享都是比較有乾貨的(但感覺工作中用不到)

首先是關於林子熠老師分享的冷啟動加速技術,聽完後這幾天也在思考分享中所說敢叫日月換新天的建立型技術與現有靜態編譯語言的對比。

演講:天下武功,唯快不破:面向雲原生應用的冷啟動加速技術

分享人: 林子熠(層風) 博士 阿里巴巴 /技術專家

Java從誕生到現在已經經過了26年,在這段時間由於Java語言功能強,峰值效能高,生態支援好的特點,在市場上取得了具有引導性的地位,在這26年,Java應用在不斷的發展演進,從最開始的單機版到web應用再到現在的service雲原生應用,在發展的過程中也不斷遇到了各種各樣新的挑戰,也帶來了機遇促進Java向前發展,在雲原生時代的應用都帶來了新的特點,比如說雲原生的應用程式短小、啟動頻繁,這都是對Java現在比較耗時的冷啟動方面比較突出的挑戰,那我們就要考慮Java應用啟動時間會這麼長,我們有什麼辦法可以解決這個問題?

先來看看Java啟動慢的原因,參考下圖。

https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

這個圖代表了Java執行時各個階段的生命週期,可以看到它要經過五個階段,首先是VM init虛擬機器的初始化階段,然後是App init應用的初始化階段,再經過App active(warmup)的應用預熱時期,在預熱一段時間後進入App active(steady)達到效能巔峰期,最後應用結束完成整個生命週期。

圖中VM init與App init就是所謂的冷啟動,紅色部分的VM虛擬機器初始化,這是逃不掉的,藍色的CL(ClassLoad),這兩個已經佔用很多時間了,接下來才慢慢的預熱再發展。

那麼我們如何針對冷啟動的根因做一些東西。

比如說我們有一種改良性的技術,在現有的Java的框架和執行模型的裡面做一些調整優化,例如App CDS技術,降低冷啟動階段的類載入開銷,去削減CL的時間達到整體時間的壓縮。

還有一種革新性的技術,靜態編譯,啟動即巔峰。

改良型——EagerAppCDS

積跬步,至千里

CDS(Class Data Sharing)是一個Java已有的技術,允許將一組類預處理為共享歸檔檔案,以便在執行時能夠進行記憶體對映以減少 Java 程式的啟動時間,當多個 Java 虛擬機器(JVM)共享相同的歸檔檔案時,還可以減少動態記憶體的佔用量,同時減少多個虛擬機器在同一個物理或虛擬的機器上執行時的資源佔用。

Java 10 在現有的 CDS 功能基礎上再次拓展,以允許應用類放置在共享存檔中。CDS 特性在原來的 bootstrap 類基礎之上,擴充套件加入了應用類的 CDS (Application Class-Data Sharing) 支援。其原理為:在啟動時記錄載入類的過程,寫入到文字檔案中,再次啟動時直接讀取此啟動文字並載入。設想如果應用環境沒有大的變化,啟動速度就會得到提升。

上圖中,Klass是一塊記憶體物件指標,指向被ClassLoader載入到類例項,傳統的CDS將這部分內容持久化到磁碟,在下次載入時直接從磁碟讀取,但起初這隻能支援System Class,不能支援Custom Class,在JDK 8u40後才開始陸續支援。

為此阿里有一套自研的Alibaba CDS,如下圖,傳統AppCDS中,如果是system class直接根據name匹配,如果是Custom Class就需要掃描Jar包,Jar包本質是一個Zip包,這就需要大量IO操作去載入,效能當然不會好。

這種方案在Custom Class越多的情況下肯定會對效能提升有更好的支援。

os: 在當日美團萬億級別微服務治理的挑戰與實踐中,曹繼光提到了美團在序列化反序列化上做的優化,通過分析,發現部分序列化和反序列化佔據整個呼叫時長的9%左右,提到了在這方面做的一些優化,最後提了一句在多例項間共享記憶體,來避免序列化與反序列化操作,雖然聽起來有點難,但是聯想到本次冷啟動加速的方向中CDS的操作,能不能直接把物件記憶體摳出來,進行類似主從同步的操作(誤)。

現狀

已在阿里雲SAE(serverless微服務PaaS)平臺應用。

應用啟動耗時降低5~45%,提升效果與啟動時類載入數量成正比。

其他改進型技術

JWarmup:共享預熱後的code cache,減小JIT開銷

PGO AOT:增強的AOT技術,改進AOT的程式碼質量

Class Preinit:類預先初始化,降低執行時初始化類的開銷

創新型——Graal VM靜態編譯技術

Graal VM是基於Java的開源高效能多語言執行平臺,擁有高效能低記憶體佔用的優點。

下圖是Java編譯技術的演進歷史,藍色部分執行在JVM中。

我們的ByteCode位元組碼在解釋執行的過程中,需要由JVM解釋執行器邊解釋邊執行,速度上當然最慢。

JIT,實時編譯,當函式執行一定次數後就放到C1+C2的編譯器中,之後這部分程式碼就不需要去解釋執行了,但編譯也是要耗費執行時間,速度也不容樂觀。

AOT,先把一部分程式碼提前由jaotc編譯好,在執行時就不需要解釋執行這部分程式碼,但這部分程式碼在jaotc時拿不到VM runtime。

再激進就是靜態編譯技術,不再需要JVM,而是SVM提供執行時環境,直接將Bytecode轉化為BinaryCode去執行。

  • 靜態編譯必須遵頊封閉性原則(the closed-world assumption)
  • 所有執行時的資訊都必須在編譯時可見
  • 兩個基本問題
    • 如何確定封閉的邊界?
    • 如何處理Java的動態特性?

如何在靜態編譯時確定執行狀態,在C/C++中,陣列的大小必須定義為一個常量,本質即編譯時可見,對於Java反射呼叫的類如何去保障編譯時可見。

針對反射的情況,Graal VM通過預執行給出了需要反射載入的類與方法,編譯時填充到緩衝區RelectionData,並且將反射替換為直接方法呼叫,在執行時從快取中查詢執行。

一個大前提就是需要預執行去掃描這部分反射呼叫的物件方法,如果掃不到,就需要自己手動去新增配置。

關於效能報告的可以自己去檢視大會PPT。