探究 Java 應(yīng)用的啟動速度優(yōu)化

一 高性能和快啟動速度拙寡,能否魚和熊掌兼得庐橙?

Java 作為一門面向?qū)ο缶幊陶Z言鼻疮,在性能方面的卓越表現(xiàn)獨(dú)樹一幟怯伊。

《Energy Efficiency across Programming Languages,How Does Energy, Time, and Memory Relate?》 這份報(bào)告調(diào)研了各大編程語言的執(zhí)行效率判沟,雖然場景的豐富程度有限耿芹,但是也能夠讓我們見微知著。

從表中挪哄,我們可以看到吧秕,Java 的執(zhí)行效率非常高,約為最快的C語言的一半迹炼。這在主流的編程語言中砸彬,僅次于C、Rust 和 C++斯入。

Java 的優(yōu)異性能得益于 Hotspot 中非常優(yōu)秀的 JIT 編譯器砂碉。Java 的 Server Compiler(C2) 編譯器是 Cliff Click 博士的作品,使用了 Sea-of-Nodes 模型刻两。而這項(xiàng)技術(shù)增蹭,也通過時(shí)間證明了它代表了業(yè)界的最先進(jìn)水平:

  • 著名的V8(JavaScript引擎)的 TurboFan 編譯器使用了相同的設(shè)計(jì),只是用更加現(xiàn)代的方式去實(shí)現(xiàn)磅摹;

  • Hotspot 使用 Graal JVMCI 做 JIT 時(shí)滋迈,性能基本與 C2 持平;

  • Azul 的商業(yè)化產(chǎn)品將 Hotspot 中的 C2 compiler 替換成 LLVM户誓,峰值性能和 C2 也是持平饼灿。

在高性能的背后,Java 的啟動性能差也令人印象深刻帝美,大家印象中的 Java 笨重緩慢的印象也大多來源于此碍彭。高性能和快啟動速度似乎有一些相悖,本文將和大家一起探究兩者是否可以兼得。

二 Java 啟動慢的根因

1 框架復(fù)雜

JakartaEE 是 Oracle 將 J2EE 捐贈給 Eclipse 基金會后的新名字硕旗。Java 在1999年推出時(shí)便發(fā)布了 J2EE 規(guī)范,EJB(Java Enterprise Beans) 定義了企業(yè)級開發(fā)所需要的安全女责、IoC漆枚、AOP、事務(wù)抵知、并發(fā)等能力墙基。設(shè)計(jì)極度復(fù)雜,最基本的應(yīng)用都需要大量的配置文件刷喜,使用非常不便残制。

隨著互聯(lián)網(wǎng)的興起,EJB 逐漸被更加輕量和免費(fèi)的 Spring 框架取代掖疮,Spring 成了 Java 企業(yè)開發(fā)的事實(shí)標(biāo)準(zhǔn)初茶。Spring 雖然定位更加輕量,但是骨子里依然很大程度地受 JakartaEE 的影響浊闪,比如早期版本大量 xml 配置的使用恼布、大量 JakartaEE 相關(guān)的注解(比如JSR 330依賴注入),以及規(guī)范(如JSR 340 Servlet API)的使用搁宾。

但 Spring 仍是一個(gè)企業(yè)級的框架折汞,我們看幾個(gè) Spring 框架的設(shè)計(jì)哲學(xué):

  • 在每一層都提供選項(xiàng),Spring 可以讓你盡可能的推遲選擇盖腿。

  • 適應(yīng)不同的視角爽待,Spring 具有靈活性,它不會強(qiáng)制為你決定該怎么選擇翩腐。它以不同的視角支持廣泛的應(yīng)用需求鸟款。

  • 保持強(qiáng)大的向后兼容性。

在這種設(shè)計(jì)哲學(xué)的影響下茂卦,必然存在大量的可配置和初始化邏輯欠雌,以及復(fù)雜的設(shè)計(jì)模式來支撐這種靈活性。我們通過一個(gè)試驗(yàn)來看:

我們跑一個(gè)spring-boot-web的helloword疙筹,通過-verbose:class可以看到依賴的class文件:

$ java -verbose:class -jar myapp-1.0-SNAPSHOT.jar | grep spring | head -n 5

class 個(gè)數(shù)到達(dá)驚人的7404個(gè)富俄。

我們再對比下 JavaScript 生態(tài),使用常用的 express 編寫一個(gè)基本應(yīng)用:

const express = require('express')

我們借用 Node 的 debug 環(huán)境變量分析:

NODE_DEBUG=module node app.js 2>&1  | head -n 5

這里只依賴了區(qū)區(qū)55個(gè) js 文件而咆。

雖然拿 spring-boot 和 express 比并不公平霍比。在 Java 世界也可以基于 Vert.X、Netty 等更加輕量的框架來構(gòu)建應(yīng)用暴备,但是在實(shí)踐中悠瞬,大家?guī)缀醵紩患偎妓鞯剡x擇 spring-boot,以便享受 Java 開源生態(tài)的便利。

2 一次編譯浅妆,到處運(yùn)行

Java 啟動慢是因?yàn)榭蚣軓?fù)雜嗎望迎?答案只能說框架復(fù)雜是啟動慢的原因之一。通過 GraalVM 的 Native Image 功能結(jié)合 spring-native 特性凌外,可以將 spring-boot 應(yīng)用的啟動時(shí)間縮短約十倍辩尊。

Java 的 Slogan 是 "Write once, run anywhere"(WORA),Java 也確實(shí)通過字節(jié)碼和虛擬機(jī)技術(shù)做到了這一點(diǎn)康辑。

WORA 使得開發(fā)者在 MacOS 上開發(fā)調(diào)試完成的應(yīng)用可以快速部署到 Linux 服務(wù)器摄欲,跨平臺性也讓 Maven 中心倉庫更加易于維護(hù),促成了 Java 開源生態(tài)的繁榮疮薇。

我們來看一下 WORA 對 Java 的影響:

  • Class Loading

Java 通過 class 來組織源碼胸墙,class 被塞進(jìn) JAR 包以便組織成模塊和分發(fā),JAR 包本質(zhì)上是一個(gè) ZIP 文件:

$ jar tf slf4j-api-1.7.25.jar | head

每個(gè) JAR 包都是功能上比較獨(dú)立的模塊按咒,開發(fā)者就可以按需依賴特定功能的 JAR迟隅,這些 JAR 通過 class path 被JVM 所知悉,并進(jìn)行加載励七。

根據(jù)<JVM Specification>玻淑,執(zhí)行到 new 或者 invokestatic 字節(jié)碼時(shí)會觸發(fā)類加載。JVM 會將控制交給 Classloader 呀伙,最常見的實(shí)現(xiàn) URLClassloader 會遍歷 JAR 包补履,去尋找相應(yīng)的 class 文件:

for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {

因此查找類的開銷,通常和 JAR 包個(gè)數(shù)成正比剿另,在大型應(yīng)用的場景下個(gè)數(shù)會上千箫锤,導(dǎo)致整體的查找耗時(shí)很高。

當(dāng)找到 class 文件后 JVM 需要校驗(yàn) class 文件的是否合法雨女,并解析成內(nèi)部可用的數(shù)據(jù)結(jié)構(gòu)谚攒,在 JVM 中叫做 InstanceKlass ,聽過 javap 窺視一下class文件包含的信息:

$ javap -p SimpleMessage.class

這個(gè)結(jié)構(gòu)包含接口氛堕、基類馏臭、靜態(tài)數(shù)據(jù)、對象的 layout讼稚、方法字節(jié)碼括儒、常量池等等。這些數(shù)據(jù)結(jié)構(gòu)都是解釋器執(zhí)行字節(jié)碼或者JIT編譯所必須的锐想。

Class initialize

當(dāng)類被加載完成后帮寻,要完成初始化才能實(shí)際創(chuàng)建對象或者調(diào)用靜態(tài)方法。類初始化可以簡單理解為靜態(tài)塊:

public class A {

上面的第一個(gè)靜態(tài)變量 JAVA_VERSION_STRING 的初始化在編譯成字節(jié)碼后也會成為靜態(tài)塊的一部分赠摇。

類初始化有如下特點(diǎn):

  • 只執(zhí)行一次固逗;

  • 有多線程嘗試訪問類時(shí)浅蚪,只有一個(gè)線程會執(zhí)行類初始化,JVM 保證其他線程都會阻塞等待初始化完成烫罩。

這些特點(diǎn)非常適合讀取配置惜傲,或者構(gòu)造一些運(yùn)行時(shí)所需要數(shù)據(jù)結(jié)構(gòu)、緩存等等贝攒,因此很多類的初始化邏輯會寫的比較復(fù)雜盗誊。

  • Just In Time compile

Java 類在被初始化后就可以實(shí)例對象,并調(diào)用對象上的方法了饿这。解釋執(zhí)行類似一個(gè)大的 switch..case 循環(huán)浊伙,性能比較差:

while (true) {

我們用 JMH 來跑一個(gè) Hessian 序列化的 Micro Benchmark 試驗(yàn):

$ java -jar benchmarks.jar hessianIO

第二次運(yùn)行的 -Xint 參數(shù)控制了我們只使用解釋器撞秋,這里差了26倍长捧,這是直接機(jī)器執(zhí)行的執(zhí)行和解釋執(zhí)行的差異帶來的。這個(gè)差距跟場景的關(guān)系很大吻贿,我們通常的經(jīng)驗(yàn)值是50倍串结。

我們來進(jìn)一步看下 JIT 的行為:

$ java -XX:+PrintFlagsFinal -version | grep CompileThreshold

這里是兩項(xiàng) JDK 內(nèi)部的 JIT 參數(shù)的數(shù)值,我們暫不對分層編譯原理做過多介紹舅列,可以參考 Stack Overflow 肌割。Tier3 可以簡單理解為(client compiler)C1,Tier4 是 C2帐要。當(dāng)一個(gè)方法解釋執(zhí)行2000次會進(jìn)行 C1 編譯把敞,當(dāng) C1 編譯后執(zhí)行15000次后就會 C2 編譯,真正達(dá)到文章開頭的 C 的一半性能完全體榨惠。

在應(yīng)用剛啟動階段奋早,方法還沒有完全被JIT編譯完成,因此大部分情況停留在解釋執(zhí)行赠橙,影響了應(yīng)用啟動的速度耽装。

三 如何優(yōu)化 Java 應(yīng)用的啟動速度

前面我們花了大量的篇幅分析了 Java 應(yīng)用啟動慢的主要原因,總結(jié)下就是:

  • 受到 JakartaEE 影響期揪,常見框架考慮復(fù)用和靈活性掉奄,設(shè)計(jì)得比較復(fù)雜;

  • 為了跨平臺性凤薛,代碼是動態(tài)加載姓建,并且動態(tài)編譯的,啟動階段加載和執(zhí)行耗時(shí)缤苫;

這兩者綜合起來造成了 Java 應(yīng)用啟動慢的現(xiàn)狀引瀑。

Python 和 Javascript 都是動態(tài)解析加載模塊的,CPyhton 甚至沒有 JIT榨馁,理論上啟動不會比 Java 快很多憨栽,但是它們并沒有使用很復(fù)雜的應(yīng)用框架,因此整體不會感受到啟動性能的問題。

雖然我們無法輕易去改變用戶對框架的使用習(xí)慣屑柔,但是可以在運(yùn)行時(shí)層面進(jìn)行增強(qiáng)屡萤,使啟動性能盡量靠近 Native image。OpenJDK 官方社區(qū)也一直在努力解決啟動性能問題掸宛,那么我們作為普通 Java 開發(fā)者死陆,是否可以借助OpenJDK的最新特性來協(xié)助我們提升啟動性能呢?

  • Class Loading

  • 通過 JarIndex 解決 JAR 包遍歷問題唧瘾,不過該技術(shù)過于古老措译,很難在現(xiàn)代的囊括了tomcat、fatJar的項(xiàng)目里使用起來

  • AppCDS 可以解決 class 文件解析處理的性能問題

  • Class Initialize: OpenJDK9 加入了 HeapArchive饰序,可以持久化一部分類初始化相關(guān)的 Heap 數(shù)據(jù)领虹,不過只有寥寥數(shù)個(gè) JDK 內(nèi)部 class (比如 IntegerCache )可以被加速,沒有開放的使用方式求豫。

  • JIT預(yù)熱: JEP295 實(shí)現(xiàn)了 AOT 編譯塌衰,但是存在 bug,使用不當(dāng)會引發(fā)程序正確性能問題蝠嘉。在性能上沒有得到很好的 tuning最疆,大部分情況下看不到效果,甚至?xí)霈F(xiàn)性能回退蚤告。

面對 OpenJDK 上述特性所存在的問題努酸,Alibaba Dragonwell 對以上各項(xiàng)技術(shù)進(jìn)行了研發(fā)優(yōu)化,并與云產(chǎn)品進(jìn)行了整合杜恰,用戶不需要投入太多精力就可以輕松地優(yōu)化啟動時(shí)間获诈。

1 AppCDS

CDS (Class Data Sharing)在Oracle JDK1.5被首次引入,在 Oracle JDK8u40 中引入了AppCDS箫章,支持JDK以外的類 烙荷,但是作為商業(yè)特性提供。隨后Oracle將AppCDS貢獻(xiàn)給了社區(qū)檬寂,在JDK10中CDS逐漸完善终抽,也支持了用戶自定義類加載器(又稱 AppCDS v2 )。

面向?qū)ο笳Z言將對象(數(shù)據(jù))和方法(對象上的操作)綁定到了一起桶至,來提供更強(qiáng)的封裝性和多態(tài)昼伴。這些特性都依賴對象頭中的類型信息來實(shí)現(xiàn),Java镣屹、Python語言都是如此圃郊。Java對象在內(nèi)存中的layout如下:

+-------------+
|  mark       |
+-------------+
|  Klass*     |
+-------------+
|  fields     |
|             |
+-------------+

mark 表示了對象的狀態(tài),包括是否被加鎖女蜈、GC年齡等等持舆。而Klass*指向了描述對象類型的數(shù)據(jù)結(jié)構(gòu) InstanceKlass :

//  InstanceKlass layout:

基于這個(gè)結(jié)構(gòu)色瘩,諸如 o instanceof String 這樣的表達(dá)式就可以有足夠的信息判斷了。要注意的是 InstanceKlass 結(jié)構(gòu)比較復(fù)雜逸寓,包含了類的所有方法居兆、field等等,方法又包含了字節(jié)碼等信息竹伸。這個(gè)數(shù)據(jù)結(jié)構(gòu)是通過運(yùn)行時(shí)解析class文件獲得的泥栖,為了保證安全性,解析class時(shí)還需要校驗(yàn)字節(jié)碼的合法性( 非通過 Javac 產(chǎn)生的方法字節(jié)碼很容易引起 JVM crash)勋篓。

CDS 可以將這個(gè)解析吧享、校驗(yàn)產(chǎn)生的數(shù)據(jù)結(jié)構(gòu)存儲(dump)到文件,在下一次運(yùn)行時(shí)重復(fù)使用譬嚣。這個(gè)dump產(chǎn)物叫做Shared Archive钢颂,以jsa后綴(Java shared archive)。

為了減少 CDS 讀取 jsa dump 的開銷孤荣,避免將數(shù)據(jù)反序列化到 InstanceKlass 的開銷甸陌,jsa 文件中的存儲layout和 InstanceKlass 對象完全一樣须揣,這樣在使用 jsa 數(shù)據(jù)時(shí)盐股,只需要將 jsa 文件映射到內(nèi)存,并且讓對象頭中的類型指針指向這塊內(nèi)存地址即可耻卡,十分高效疯汁。

Object:

AppCDS 對 customer class loader 力不從心

jsa 中存儲的 InstanceKlass 是對class文件解析的產(chǎn)物。對于 boot classloader (就是加載jre/lib/rt.jar下面的類的classloader)和 system(app) classloader (加載-classpath下面的類的 classloader )卵酪,CDS有內(nèi)部機(jī)制可以跳過對 class文件 的讀取幌蚊,僅僅通過類名在 jsa 文件中匹配對應(yīng)的數(shù)據(jù)結(jié)構(gòu)。

Java 還提供用戶自定義類加載器(custom class loader)的機(jī)制溃卡,用戶通過Override自己的 Classloader.loadClass() 方法可以高度定制化獲取類的邏輯溢豆,比如從網(wǎng)絡(luò)上獲取、直接在代碼中動態(tài)生成都是可行的瘸羡。為了增強(qiáng)AppCDS的安全性漩仙,避免因?yàn)閺腃DS加載了類定義反而獲得了非預(yù)期的類,AppCDS customer class loader需要經(jīng)過如下步驟:

  1. 調(diào)用用戶定義的 Classloader.loadClass() 犹赖,拿到class byte stream

  2. 計(jì)算class byte stream的checksum队他,與jsa中的同類名結(jié)構(gòu)的checksum比較

  3. 如果匹配成功則返回jsa中的 InstanceKlass ,否則繼續(xù)使用slow path解析class文件

我們看到許多場景下峻村,上述的第一步占據(jù)了類加載耗時(shí)的大頭麸折,此時(shí) AppCDS 就顯得力不從心了。舉例來說:

bar.jar

class path 包含如上的三個(gè)jar包粘昨,在加載class com.foo.Foo 時(shí)垢啼,大部分Classloader實(shí)現(xiàn)(包括URLClassloader窜锯、tomcat、spring-boot)都選擇了最簡單的策略(過早的優(yōu)化是萬惡之源): 按照jar包出現(xiàn)在磁盤的順序逐個(gè)嘗試抽取 com/foo/Foo.class 這個(gè)文件芭析。

JAR 包使用了 zip 格式作為存儲衬浑,每次類加載都需要遍歷classpath下的 JAR 包們,嘗試從 zip 中抽取單個(gè)文件放刨,來確保存在的類可以被找到工秩。假設(shè)有N個(gè) JAR 包,那么平均一個(gè)類加載需要嘗試訪問N/2個(gè)zip文件进统。

在我們的一個(gè)真實(shí)場景下助币,N到達(dá)2000,此時(shí) JAR 包查找開銷非常大螟碎,并且遠(yuǎn)大于InstanceKlass解析的開銷眉菱。面對此類場景 AppCDS 技術(shù)就力不從心了。

JAR Index

根據(jù) jar文件規(guī)范 掉分,JAR 文件是一種使用 zip封裝俭缓,并使用文本在 META-INF 目錄存儲元信息的格式。該格式在設(shè)計(jì)時(shí)已經(jīng)考慮了應(yīng)對上述的查找場景酥郭,這項(xiàng)技術(shù)叫做 JAR Index 华坦。

假設(shè)我們要在上述的bar.jar、baz.jar不从、foo.jar中查找一個(gè)class惜姐,如果能夠通過類型com.foo.Foo,立刻推斷出具體在哪個(gè)jar包椿息,就可以避免上述的掃描開銷了歹袁。

JarIndex-Version: 1.0

通過 JAR Index 技術(shù),可以生成出上述的索引文件INDEX.LIST寝优。加載到內(nèi)存后成為一個(gè)HashMap:

com/bar --> bar.jar

當(dāng)我們看到類名 com.foo.Foo 条舔,可以根據(jù)包名 com.foo 從索引中得知具體的jar包foo.jar,迅速抽取class文件乏矾。

Jar Index 技術(shù)看似解決了我們的問題孟抗,但是這項(xiàng)技術(shù)十分古老,很難在現(xiàn)代應(yīng)用中被使用起來:

  • jar i 根據(jù) META-INF/MANIFEST.MF 中的 Class-Path 屬性產(chǎn)生索引文件妻熊,現(xiàn)代項(xiàng)目幾乎不維護(hù)這個(gè)屬性

  • 只有 URLClassloader 支持JAR Index

  • 要求帶索引的jar盡量出現(xiàn)在 classpath 的前面

Dragonwell 通過 agent 注入使得 INDEX.LIST 能夠被正確地生成夸浅,并出現(xiàn)在 classpath 的合適位置來幫助應(yīng)用提升啟動性能。

2 類提前初始化

類的 static block 中的代碼執(zhí)行我們稱之為類初始化扔役,類加載完成后必須執(zhí)行完初始化代碼才能被使用(創(chuàng)建instance帆喇、調(diào)用 static 方法)。

很多類的初始化本質(zhì)上只是構(gòu)造一些static field:

class IntegerCache {
    static final Integer cache[];
    static {
        Integer[] c = new Integer[size];
        int j = low;
        for(int k = 0; k < c.length; k++)
            c[k] = new Integer(j++);
        cache = c;
    }
}

我們知道 JDK 對 box type 中常用的一段區(qū)間有緩存亿胸,避免過多的重復(fù)創(chuàng)建坯钦,這段數(shù)據(jù)就需要提前構(gòu)造好预皇。由于這些方法只會被執(zhí)行一次,因此是以純解釋的方式執(zhí)行的婉刀,如果可以持久化幾個(gè)static字段的方式來避免調(diào)用類初始化器吟温,我們就可以拿到提前初始化好的類,減少啟動時(shí)間突颊。

將持久化加載到內(nèi)存使用最高效的方式是內(nèi)存映射:

int fd = open("archive_file", O_READ);
struct person *persons = mmap(NULL, 100 * sizeof(struct person),
                              PROT_READ, fd, 0);
int age = persons[5].age;

C語言幾乎是直接面向內(nèi)存來操作數(shù)據(jù)的鲁豪,而Java這樣的高級語言都將內(nèi)存抽象成了對象,有mark律秃、Klass*等元信息爬橡,每次運(yùn)行之間都存在一定的變化,因此需要更加復(fù)雜的機(jī)智來獲得高效的對象持久化棒动。

Heap Archive簡介

OpenJDK9 引入了 HeapArchive能力 糙申,OpenJDK12中heap archive 被 正式使用 菩收。顧名思義玄组,Heap Archive技術(shù)可以將堆上的對象持久化存儲下來。

對象圖被提前被構(gòu)建好后放進(jìn)archive味滞,我們將這個(gè)階段稱為dump粱锐;而使用archive里的數(shù)據(jù)稱為運(yùn)行時(shí)疙挺。dump和運(yùn)行時(shí)通常不是一個(gè)進(jìn)程,但在某些場景下也可以是同一個(gè)進(jìn)程卜范。

回憶下使用AppCDS后的內(nèi)存布局衔统,對象的Klass*指針指向了SharedArchive中的的數(shù)據(jù)鹿榜。AppCDS對 InstanceKlass 這個(gè)元信息進(jìn)行了持久化海雪,如果想要復(fù)用持久化的對象,那么對象頭的類型指針必須也要指向一塊被持久化過的元信息舱殿,因此HeapArchive技術(shù)是依賴AppCDS的奥裸。

為了適應(yīng)多種場景,OpenJDK的HeapArchive還提供了Open和Closed兩種級別:

image

上圖是允許的引用關(guān)系:

  • Closed Archive

  • 不允許引用Open Archive 和Heap中的對象

  • 可以引用Closed Archive內(nèi)部的對象

  • 只讀沪袭,不可寫

  • Open Archive

  • 可以引用任何對象

  • 可寫

這樣設(shè)計(jì)的原因是對于一些只讀結(jié)構(gòu)湾宙,放在Closed Archive 中可以做到對GC完全無開銷。

為什么只讀冈绊?想象一下侠鳄,假如Closed Archive中的對象A引用了heap中的對象B,那么當(dāng)對象B移動時(shí)死宣,GC需要修正A中指向B的field伟恶,這會帶來GC開銷。

利用 Heap Archive 提前做類初始化

支持這種結(jié)構(gòu)后毅该,在類加載后博秫,將static變量指向被Archive的對象潦牛,即可完成類初始化:

class Foo {

3 AOT編譯

除去類的加載,方法的前幾次執(zhí)行因?yàn)闆]有被JIT編譯器給編譯挡育,字節(jié)碼在解釋模式下執(zhí)行巴碗。根據(jù)本文上半部分的分析,解釋執(zhí)行速度約為JIT編譯后的幾十分之一即寒,代碼解釋執(zhí)行慢也啟動慢的一大元兇橡淆。

傳統(tǒng)的C/C++等語言都是直接編譯到目標(biāo)平臺的native機(jī)器碼。隨著大家意識到Java母赵、JS等解釋器JIT語言的啟動預(yù)熱問題明垢,通過AOT將字節(jié)碼直接編譯到native代碼這種方式逐漸進(jìn)入公眾視野。

wasm市咽、GraalVM痊银、OpenJDK都不同程度地支持了AOT編譯,我們主要圍繞 JEP295 引入的jaotc工具優(yōu)化啟動速度施绎。

注 意這里的術(shù) 語使用:

JEP295 使用 AOT 是將 class 文件中的方法逐個(gè)編譯到 native 代碼片段溯革,通過 Java 虛擬機(jī)在加載某個(gè)類后替換方法的的入口到 AOT 代碼。

而 GraalVM 的的 Native Image 功能是更加徹底的靜態(tài)編譯谷醉,通過一個(gè)用 Java 代碼編寫的小型運(yùn)行時(shí) SubstrateVM 致稀,該運(yùn)行時(shí)和應(yīng)用代碼一起被靜態(tài)編譯到可執(zhí)行的文件 ( 類似 Go) ,不再依賴 JVM 俱尼。該做法也是一種 AOT 抖单,但是為了區(qū)分術(shù)語,這里的 AOT 單指 JEP295 的方式遇八。

AOT特性初體驗(yàn)

通過 JEP295 的介紹矛绘,我們可以快速體驗(yàn)AOT

cat > HelloWorld.java <<EOF

jaotc 命令會調(diào)用Graal編譯器對字節(jié)碼進(jìn)行編譯,產(chǎn)生 libHelloWorld.so 文件刃永。這里產(chǎn)生的so文件容易讓人誤以為會直接像JNI一樣調(diào)用進(jìn)編譯好的庫代碼货矮。但是這里并沒有完全使用ld的加載機(jī)制來運(yùn)行代碼,so文件更像是當(dāng)做一個(gè) native 代碼的容器斯够。hotsopt runtime 在加載 AOT so 后需要進(jìn)行進(jìn)一步的動態(tài)鏈接囚玫。在類加載后hotspot 會自動關(guān)聯(lián) AOT 代碼入口,對于下次方法調(diào)用使用 AOT 版本读规。而 AOT 生成的代碼也會主動與 hotspot 運(yùn)行時(shí)交互抓督,在aot、解釋器束亏、JIT 代碼間相互跳轉(zhuǎn)铃在。

1)AOT 的一波三折

看起來 JEP295 已經(jīng)實(shí)現(xiàn)了一套完備的AOT體系,但是為何不見這項(xiàng)技術(shù)被大規(guī)模使用枪汪?在 OpenJDK 的各項(xiàng)新特性中涌穆,AOT 算得上是命途多舛怔昨。

2)多 Classloader 問題

JDK-8206963 : bug with multiple class loaders

這是在設(shè)計(jì)上沒有考慮到Java的多 Classloader 場景,當(dāng)多個(gè) Classloader 加載的同名類都使用了 AOT 后宿稀,他們的 static field 是共享的趁舀,而根據(jù) Java 語言的設(shè)計(jì),這部分?jǐn)?shù)據(jù)應(yīng)該是隔開的祝沸。

由于沒有可以快速修復(fù)這個(gè)問題的方案矮烹,OpenJDK 僅僅是添加了如下代碼:

ClassLoaderData* cld = ik->class_loader_data();

對于用戶自定義類加載器不允許使用 AOT。從這里已經(jīng)可以初步看出該特性在社區(qū)層面已經(jīng)逐漸缺乏維護(hù)罩锐。

在這種情況下奉狈,雖然通過 class-path 指定的類依然可以使用 AOT,但是我們常用的 spring-boot涩惑、Tomcat 等框架都需要通過 Custom Classloader 加載應(yīng)用代碼仁期。可以說這一改變切掉了 AOT 的一大塊場景竭恬。

3)缺乏調(diào)優(yōu)和維護(hù)跛蛋,退回成實(shí)驗(yàn)特性

JDK-8227439 : Turn off AOT by default

JEP 295 AOT is still experimental, and while it can be useful for startup/warmup when used with custom generated archives tailored for the application, experimental data suggests that generating shared libraries at a module level has overall negative impact to startup, dubious efficacy for warmup and severe static footprint implications.

從此打開 AOT 需要添加 experimental 參數(shù):

java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=...

根據(jù) issue 的描述,這項(xiàng)特性編譯整個(gè)模塊的情況下痊硕,對啟動速度和內(nèi)存占用都起到了反作用赊级。我們分析的原因如下:

  • Java 語言本身過分復(fù)雜,動態(tài)類加載等運(yùn)行時(shí)機(jī)制導(dǎo)致 AOT 代碼沒法運(yùn)行得像預(yù)期一樣快

  • AOT 技術(shù)作為階段性的項(xiàng)目在進(jìn)入 Java 9 之后并沒有被長期維護(hù)岔绸,缺乏必要的調(diào)優(yōu)(反觀AppCDS一直在迭代優(yōu)化)

4)JDK16 中被刪除

JDK-8255616 : Disable AOT and Graal in Oracle OpenJDK

在 OpenJDK16 發(fā)布前夕理逊,Oracle正式?jīng)Q定不再維護(hù)這項(xiàng)技術(shù):

We haven't seen much use of these features, and the effort required to support and enhance them is significant. 

其根本原因還是這項(xiàng)基于缺乏必要的優(yōu)化和維護(hù)。而對于 AOT 相關(guān)的未來的規(guī)劃盒揉,只能從只言片語中推測將來Java的AOT 有兩種技術(shù)方向:

  • 在 OpenJDK 的 C2 基礎(chǔ)上做 AOT

  • 在 GraalVM 的 native-image 上支持完整的 Java 語言特性晋被,需要 AOT 的用戶逐漸從 OpenJDK 過渡到native-image

上述的兩個(gè)技術(shù)方向都沒法在短期內(nèi)看到進(jìn)展,因此 Dragonwell 的技術(shù)方向是讓現(xiàn)有的 JEP295 更好地工作预烙,為用戶帶來極致的啟動性能墨微。

5)Dragonwell 上的快速啟動

Dragonwell 的快速啟動特性攻關(guān)了 AppCDS、AOT 編譯技術(shù)上的弱點(diǎn)扁掸,并基于 HeapArchive 機(jī)制研發(fā)了類提前初始化特性。這些特性將 JVM 可見的應(yīng)用啟動耗時(shí)幾乎全部消除最域。

此外谴分,因?yàn)樯鲜鰩醉?xiàng)技術(shù)都符合 trace-dump-replay 的使用模式,Dragonwell 將上述啟動加速技術(shù)統(tǒng)一了流程镀脂,并且集成到了 SAE 產(chǎn)品中牺蹄。

四 SAE x Dragonwell : Serverless with Java 啟動加速最佳實(shí)踐

有了好的食材,還需要相匹配的佐料薄翅,以及一位烹飪大師沙兰。

將 Dragonwell 的啟動加速技術(shù)和和以彈性著稱的 Serverless 技術(shù)相結(jié)合更相得益彰氓奈,同時(shí)共同落地在微服務(wù)應(yīng)用的全生命周期管理中,才能發(fā)揮他們縮短應(yīng)用端到端啟動時(shí)間的作用鼎天,因此 Dragonwell 選擇了 SAE 來落地其啟動加速技術(shù)舀奶。

S AE (Serverless 應(yīng)用引擎)是首款面向 Serverless 的 PaaS 平臺,他可以:

  • Java 軟件包部署:零代碼改造享受微服務(wù)能力斋射,降低研發(fā)成本

  • Serverless 極致彈性:資源免運(yùn)維育勺,快速擴(kuò)容應(yīng)用實(shí)例, 降低運(yùn)維與學(xué)習(xí)成本

1 難點(diǎn)分析

通過分析罗岖,我們發(fā)現(xiàn)微服務(wù)的用戶在應(yīng)用啟動層面面臨著一些難題:

  • 軟件包大:幾百 MB 甚至 GB 級別

  • 依賴包多:上百個(gè)依賴包涧至,幾千個(gè) Class

  • 加載耗時(shí):從磁盤加載依賴包,再到 Class 按需加載桑包,最高可占啟動耗時(shí)的一半

借助 Dragonwell 快速啟動能力南蓬,SAE 為 Serverless Java 應(yīng)用提供了一套,讓應(yīng)用盡可能加速啟動的最佳實(shí)踐哑了,讓開發(fā)者更專注于業(yè)務(wù)開發(fā):

  • Java 環(huán)境 + JAR/WAR 軟件包部署:集成 Dragonwell 11 蓖康,提供加速啟動環(huán)境

  • JVM 快捷設(shè)置:支持一鍵開啟快速啟動,簡化操作

  • NAS 網(wǎng)盤:支持跨實(shí)例加速垒手,在新包部署時(shí)蒜焊,加速新啟動實(shí)例/分批發(fā)布啟動速度

2 加速效果

我們選擇一些微服務(wù)、復(fù)雜依賴的業(yè)務(wù)場景典型 Demo 或內(nèi)部應(yīng)用科贬,測試啟動效果泳梆,發(fā)現(xiàn)應(yīng)用普遍能降低 5%~45% 的啟動耗時(shí)。若應(yīng)用啟動榜掌,存在下列場景优妙,會有明顯加速效果:

  • 類加載多(spring-petclinic 啟動加載約 12000+ classes)

  • 依賴外部數(shù)據(jù)越少

3 客戶案例

阿里巴巴搜索推薦 Serverless 平臺

阿里內(nèi)部的搜索推薦 Serverless 平臺通過類加載隔離機(jī)制,將多個(gè)業(yè)務(wù)的合并部署在同一個(gè) Java 虛擬機(jī)中憎账。調(diào)度系統(tǒng)會按需地將業(yè)務(wù)代碼合并部署到空閑的容器中套硼,讓多個(gè)業(yè)務(wù)可以共享同一個(gè)資源池,大大提高部署密度和整體的 CPU 使用率胞皱。

由于要支撐大量不同的業(yè)務(wù)研發(fā)運(yùn)行邪意,平臺本身需要提供足夠豐富的功能,如緩存反砌、RPC調(diào)用雾鬼。因此搜索推薦Serverless 平臺的每個(gè) JVM 都需要拉起類似 Pandora Boot 的中間件隔離容器,這將加載大量的類宴树,拖累了平臺自身的啟動速度策菜。當(dāng)突增的需求進(jìn)入,調(diào)度系統(tǒng)需要拉起更多容器以供業(yè)務(wù)代碼部署,此時(shí)容器本身的啟動時(shí)間就顯得尤為重要又憨。

基于 Dragonwell 的快速啟動技術(shù)翠霍,搜索推薦平臺在預(yù)發(fā)布環(huán)境會執(zhí)行 AppCDS、Jarindex 等優(yōu)化蠢莺,將產(chǎn)生的 archive 文件打入容器鏡像中寒匙,這樣每一個(gè)容器在啟動時(shí)都能享受加速,減少約30%的啟動耗時(shí)浪秘。

潮牌秒殺SAE極致彈性

某外部客戶蒋情,借助 SAE 提供的 Jar 包部署與 Dragonwell 11,快速迭代上線了某潮牌商場 App耸携。

在面對大促秒殺時(shí)棵癣,借助 SAE Serverless 極致彈性,與應(yīng)用指標(biāo) QPS RT 指標(biāo)彈性能力夺衍,輕松面對 10 倍以上快速擴(kuò)容需求狈谊;同時(shí)一鍵開啟 Dragonwell 增強(qiáng)的 AppCDS 啟動加速能力,降低 Java 應(yīng)用 20% 以上啟動耗時(shí)沟沙,進(jìn)一步加速應(yīng)用啟動河劝,保證業(yè)務(wù)平穩(wěn)健康運(yùn)行。

五 總結(jié)

Dragonwell 上的快速啟動技術(shù)方向上完全基于 OpenJDK 社區(qū)的工作矛紫,對各項(xiàng)功能進(jìn)行了細(xì)致的優(yōu)化與 bugfix赎瞎,并降低了上手的難度。這樣做既保證了對標(biāo)準(zhǔn)的兼容颊咬,避免內(nèi)部定制务甥,也能夠?yàn)殚_源社區(qū)做出貢獻(xiàn)。

作為基礎(chǔ)軟件喳篇,Dragonwell 只能生成/使用磁盤上的 archive 文件敞临。結(jié)合 SAE 對 Dragonwell 的無縫集成,JVM 配置麸澜、archive 文件的分發(fā)都被自動化挺尿。客戶可以輕松享受應(yīng)用加速帶來的技術(shù)紅利炊邦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末编矾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铣耘,更是在濱河造成了極大的恐慌洽沟,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜗细,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炉媒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門踪区,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吊骤,你說我怎么就攤上這事缎岗。” “怎么了白粉?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵传泊,是天一觀的道長。 經(jīng)常有香客問我鸭巴,道長眷细,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任鹃祖,我火速辦了婚禮溪椎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恬口。我一直安慰自己校读,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布祖能。 她就那樣靜靜地躺著歉秫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪养铸。 梳的紋絲不亂的頭發(fā)上雁芙,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機(jī)與錄音揭厚,去河邊找鬼却特。 笑死,一個(gè)胖子當(dāng)著我的面吹牛筛圆,可吹牛的內(nèi)容都是我干的裂明。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼太援,長吁一口氣:“原來是場噩夢啊……” “哼闽晦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起提岔,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤仙蛉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后碱蒙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荠瘪,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夯巷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哀墓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趁餐。...
    茶點(diǎn)故事閱讀 40,769評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖篮绰,靈堂內(nèi)的尸體忽然破棺而出后雷,到底是詐尸還是另有隱情,我是刑警寧澤吠各,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布臀突,位于F島的核電站,受9級特大地震影響贾漏,放射性物質(zhì)發(fā)生泄漏候学。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一磕瓷、第九天 我趴在偏房一處隱蔽的房頂上張望盒齿。 院中可真熱鬧,春花似錦困食、人聲如沸边翁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽符匾。三九已至,卻和暖如春瘩例,著一層夾襖步出監(jiān)牢的瞬間啊胶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工垛贤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焰坪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓聘惦,卻偏偏與公主長得像某饰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子善绎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容