QCon筆記~《天下武功,唯快不破——面向云原生應(yīng)用的Java冷啟動加速技術(shù)》
上周去聽了QCon全球開發(fā)大會,其中有幾場印象比較深刻的分享,除去幾個比較概念化的話題,在Java技術(shù)演進這個Topic里的幾個分享都是比較有干貨的(但感覺工作中用不到
)
首先是關(guān)于林子熠老師分享的冷啟動加速技術(shù),聽完后這幾天也在思考分享中所說敢叫日月?lián)Q新天的創(chuàng)建型技術(shù)與現(xiàn)有靜態(tài)編譯語言的對比。
演講: 天下武功,唯快不破:面向云原生應(yīng)用的冷啟動加速技術(shù)
分享人: 林子熠(層風(fēng)) 博士 阿里巴巴 /技術(shù)專家
Java從誕生到現(xiàn)在已經(jīng)經(jīng)過了26年,在這段時間由于Java語言功能強,峰值性能高,生態(tài)支持好的特點,在市場上取得了具有引導(dǎo)性的地位,在這26年,Java應(yīng)用在不斷的發(fā)展演進,從最開始的單機版到web應(yīng)用再到現(xiàn)在的service云原生應(yīng)用,在發(fā)展的過程中也不斷遇到了各種各樣新的挑戰(zhàn),也帶來了機遇促進Java向前發(fā)展,在云原生時代的應(yīng)用都帶來了新的特點,比如說云原生的應(yīng)用程序短小、啟動頻繁,這都是對Java現(xiàn)在比較耗時的冷啟動方面比較突出的挑戰(zhàn),那我們就要考慮Java應(yīng)用啟動時間會這么長,我們有什么辦法可以解決這個問題?
先來看看Java啟動慢的原因,參考下圖。
https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

這個圖代表了Java運行時各個階段的生命周期,可以看到它要經(jīng)過五個階段,首先是VM init虛擬機的初始化階段,然后是App init應(yīng)用的初始化階段,再經(jīng)過App active(warmup)的應(yīng)用預(yù)熱時期,在預(yù)熱一段時間后進入App active(steady)達到性能巔峰期,最后應(yīng)用結(jié)束完成整個生命周期。
圖中VM init與App init就是所謂的冷啟動,紅色部分的VM虛擬機初始化,這是逃不掉的,藍色的CL(ClassLoad),這兩個已經(jīng)占用很多時間了,接下來才慢慢的預(yù)熱再發(fā)展。
那么我們?nèi)绾吾槍鋯拥母蜃鲆恍〇|西。
改良型——EagerAppCDS
積跬步,至千里
CDS(Class Data Sharing)是一個Java已有的技術(shù),允許將一組類預(yù)處理為共享歸檔文件,以便在運行時能夠進行內(nèi)存映射以減少 Java 程序的啟動時間,當(dāng)多個 Java 虛擬機(JVM)共享相同的歸檔文件時,還可以減少動態(tài)內(nèi)存的占用量,同時減少多個虛擬機在同一個物理或虛擬的機器上運行時的資源占用。
Java 10 在現(xiàn)有的 CDS 功能基礎(chǔ)上再次拓展,以允許應(yīng)用類放置在共享存檔中。CDS 特性在原來的 bootstrap 類基礎(chǔ)之上,擴展加入了應(yīng)用類的 CDS (Application Class-Data Sharing) 支持。其原理為:在啟動時記錄加載類的過程,寫入到文本文件中,再次啟動時直接讀取此啟動文本并加載。設(shè)想如果應(yīng)用環(huán)境沒有大的變化,啟動速度就會得到提升。

上圖中,Klass是一塊內(nèi)存對象指針,指向被ClassLoader加載到類實例,傳統(tǒng)的CDS將這部分內(nèi)容持久化到磁盤,在下次加載時直接從磁盤讀取,但起初這只能支持System Class,不能支持Custom Class,在JDK 8u40后才開始陸續(xù)支持。
為此阿里有一套自研的Alibaba CDS,如下圖,傳統(tǒng)AppCDS中,如果是system class直接根據(jù)name匹配,如果是Custom Class就需要掃描Jar包,Jar包本質(zhì)是一個Zip包,這就需要大量IO操作去加載,性能當(dāng)然不會好。
這種方案在Custom Class越多的情況下肯定會對性能提升有更好的支持。

os: 在當(dāng)日美團萬億級別微服務(wù)治理的挑戰(zhàn)與實踐中,曹繼光提到了美團在序列化反序列化上做的優(yōu)化,通過分析,發(fā)現(xiàn)部分序列化和反序列化占據(jù)整個調(diào)用時長的9%左右,提到了在這方面做的一些優(yōu)化,最后提了一句在多實例間共享內(nèi)存,來避免序列化與反序列化操作,雖然聽起來有點難,但是聯(lián)想到本次冷啟動加速的方向中CDS的操作,能不能直接把對象內(nèi)存摳出來,進行類似主從同步的操作(誤)。
現(xiàn)狀
已在阿里云SAE(serverless微服務(wù)PaaS)平臺應(yīng)用。
應(yīng)用啟動耗時降低5~45%,提升效果與啟動時類加載數(shù)量成正比。

其他改進型技術(shù)
JWarmup:共享預(yù)熱后的code cache,減小JIT開銷
PGO AOT:增強的AOT技術(shù),改進AOT的代碼質(zhì)量
Class Preinit:類預(yù)先初始化,降低運行時初始化類的開銷
創(chuàng)新型——Graal VM靜態(tài)編譯技術(shù)
敢叫日月?lián)Q新天
Graal VM是基于Java的開源高性能多語言運行平臺,擁有高性能低內(nèi)存占用的優(yōu)點。
下圖是Java編譯技術(shù)的演進歷史,藍色部分運行在JVM中。

我們的ByteCode字節(jié)碼在解釋執(zhí)行的過程中,需要由JVM解釋執(zhí)行器邊解釋邊執(zhí)行,速度上當(dāng)然最慢。
JIT,實時編譯,當(dāng)函數(shù)執(zhí)行一定次數(shù)后就放到C1+C2的編譯器中,之后這部分代碼就不需要去解釋執(zhí)行了,但編譯也是要耗費運行時間,速度也不容樂觀。
AOT,先把一部分代碼提前由jaotc編譯好,在運行時就不需要解釋執(zhí)行這部分代碼,但這部分代碼在jaotc時拿不到VM runtime。
再激進就是靜態(tài)編譯技術(shù),不再需要JVM,而是SVM提供運行時環(huán)境,直接將Bytecode轉(zhuǎn)化為BinaryCode去執(zhí)行。

靜態(tài)編譯必須遵守封閉性原則(the closed-world assumption)
所有運行時的信息都必須在編譯時可見
兩個基本問題
如何確定封閉的邊界?
如何處理Java的動態(tài)特性?
如何在靜態(tài)編譯時確定運行狀態(tài),在C/C++中,數(shù)組的大小必須定義為一個常量,本質(zhì)即編譯時可見,對于Java反射調(diào)用的類如何去保障編譯時可見。


針對反射的情況,Graal VM通過預(yù)執(zhí)行給出了需要反射加載的類與方法,編譯時填充到緩沖區(qū)RelectionData,并且將反射替換為直接方法調(diào)用,在運行時從緩存中查找執(zhí)行。
一個大前提就是需要預(yù)執(zhí)行去掃描這部分反射調(diào)用的對象方法,如果掃不到,就需要自己手動去添加配置。
關(guān)于性能報告的可以自己去查看大會PPT。
