Spring Boot毫無(wú)疑問(wèn)是JAVA后端開(kāi)發(fā)的第一大框架,基于Spring Boot有著一套完整的工具鏈,各種各樣的starter。對(duì)于日常業(yè)務(wù)開(kāi)發(fā)而言,可以說(shuō)是輪子很全。
但隨著云原生時(shí)代的到來(lái),Spring Boot應(yīng)用或者說(shuō)是Java應(yīng)用卻暴露出了一些問(wèn)題,其中比較突出的有:
- 啟動(dòng)慢
- 應(yīng)用內(nèi)存占用多
其中啟動(dòng)慢的主要原因:代碼編譯。
當(dāng)然對(duì)于Spring Boot來(lái)說(shuō),Bean實(shí)例注入也會(huì)花費(fèi)一定的時(shí)間,但花費(fèi)時(shí)間相比編譯會(huì)小的多。大家可以通過(guò)開(kāi)啟延遲初始化試試。
spring:
main:
lazy-initialization: true
Spring Boot 2.2開(kāi)始支持。
個(gè)人本地開(kāi)啟延遲初始化之后,啟動(dòng)能快了1~2秒,整個(gè)啟動(dòng)時(shí)間10秒左右。
測(cè)試機(jī)配置:i7-6500U 2.50@GHz 內(nèi)存:16G
內(nèi)存占用多主要是內(nèi)存占用后不會(huì)歸還操作系統(tǒng),這個(gè)正在逐步改善:
- G1 JDK12及之后 已支持
- ZGC JDK13及之后 已支持
由于Java語(yǔ)言的特性及Spring Boot的一些實(shí)現(xiàn)方式,決定了即便是開(kāi)啟了G1/ZGC的未使用內(nèi)存及時(shí)歸還操作系統(tǒng),Spring Boot的內(nèi)存占用,仍然遠(yuǎn)大于Golang這種編譯型語(yǔ)言。
2017年9月,Java 9發(fā)布,在Java 9中引入了AOT(Ahead-of-Time Compilation)。
AOT在內(nèi)部使用是通過(guò)GraalVM來(lái)生成代碼的。
但對(duì)于普通用戶(hù)而言通過(guò)Java的AOT去編譯Spring程序還是不可行的。
那么有沒(méi)有一種比較優(yōu)雅的解決方案呢?既能使用Spring Boot又能像Golang一樣啟動(dòng)快、內(nèi)存占用低?
有朋友可能想到了Quarkus、Micronaut,但這兩個(gè)框架如果是從頭開(kāi)始開(kāi)發(fā),可以考慮一下,但還是要注意兩點(diǎn):
- 需要去學(xué)習(xí)使用
- 某些庫(kù)有可能不支持
其實(shí),Java想要解決云原生時(shí)代的問(wèn)題,目前的方案基本都是基于GraalVM來(lái)的,不管是Quarkus還是Micronaut都是。
那么,Spring Boot有沒(méi)有類(lèi)似的方案呢?
答案是有的。
在
spring-projects-experimental Organizations下有這么一個(gè)項(xiàng)目:spring-graalvm-native
目前已發(fā)布到0.7.0 release,不過(guò)從github的文檔中可以看到這個(gè)項(xiàng)目的狀態(tài)仍然是alpha,也就是說(shuō)目前用到生產(chǎn)中還是為時(shí)過(guò)早。

希望能早日spring-graalvm-native能早日發(fā)布生產(chǎn)可用版本吧。
graalvm+AOT如此美好?
其實(shí),GraalVM目前來(lái)看還是有一些局限的:
Not Supported
- Dynamic Class Loading/Unloading
- Runtime Bytecode Generation *
- InvokeDynamic Bytecode and Method Handles
- ……
Require Configuration
- Resource Access
- Reflection
- Dynamic Proxy(JDK,not CGLIB)
- JNI (Java Native Interface)
- ……
更詳細(xì)限制可以看:
https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md
同時(shí),由于提前編譯無(wú)法像JIT那樣獲取到運(yùn)行時(shí)的信息,所以在做Profile-Guided Optimization,PGO時(shí),會(huì)更麻煩。
具體做法:
https://www.graalvm.org/docs/release-notes/19_2/
JIT會(huì)做的典型的PGO1:
- type-feedback optimization:主要針對(duì)多態(tài)的面向?qū)ο蟪绦騺?lái)做優(yōu)化。根據(jù)profile收集到的receiver type信息來(lái)把原本多態(tài)的虛方法調(diào)用點(diǎn)(virtual method call site)或?qū)傩栽L(fǎng)問(wèn)點(diǎn)(property access site)根據(jù)類(lèi)型來(lái)去虛化(devirtualize)。
- single-value profiling:這個(gè)相對(duì)少見(jiàn)一些。它的思路是有些參數(shù)、函數(shù)返回值可能在一次運(yùn)行中只會(huì)遇到一個(gè)具體值。如果是這樣的話(huà)可以把那個(gè)具體值給記錄下來(lái),然后在JIT編譯時(shí)把它當(dāng)作常量來(lái)做優(yōu)化,于是常見(jiàn)的常量相關(guān)優(yōu)化(常量折疊、條件常量傳播等)就可以針對(duì)一個(gè)靜態(tài)意義上本來(lái)不是常量的值來(lái)做了。branch-profile-based code scheduling:主要目的是把“熱”的(頻繁執(zhí)行的)代碼路徑集中放在一起,而把“冷”的(不頻繁執(zhí)行的)代碼路徑放到別的地方。AOT編譯的話(huà)常常會(huì)利用一些靜態(tài)的啟發(fā)條件來(lái)猜測(cè)哪些路徑比較熱,或者讓用戶(hù)指定哪些路徑比較熱(例如likely()/unlikely()宏),而JIT搭配PGO的話(huà)可以有比較準(zhǔn)確的路徑熱度信息,對(duì)應(yīng)可以做的優(yōu)化也就更吻合實(shí)際執(zhí)行情況,于是效果會(huì)更好。
- profile-guided inlining heuristics:根據(jù)profile信息得知函數(shù)調(diào)用點(diǎn)的熱度,從而影響內(nèi)聯(lián)決策——對(duì)某個(gè)調(diào)用點(diǎn),到底值不值得把目標(biāo)函數(shù)內(nèi)聯(lián)進(jìn)來(lái)。
- implicit exception:隱式異常,例如Java/C#的空指針異常檢查,又例如Java/C#的除以零檢查。這些異常如果在某塊代碼里從來(lái)沒(méi)有發(fā)生過(guò),就可以用更快的方式來(lái)實(shí)現(xiàn),而不必生成顯式檢查代碼。但如果在某塊代碼經(jīng)常發(fā)生這種異常,則顯式檢查會(huì)更快。
附錄:
1.
JIT會(huì)做的典型的PGO
https://www.zhihu.com/question/52572852??
個(gè)人思考:
其實(shí),通過(guò)openjdk jeps及spring boot的一些實(shí)驗(yàn)性的項(xiàng)目可以看出,Java正在實(shí)現(xiàn)一些新的特性:比如本文提到的AOT,Loom來(lái)解決Java的一些痛點(diǎn)。
但這些新的特性具體什么時(shí)候能用于生產(chǎn)還是一個(gè)未知數(shù)。
相對(duì)于Golang,在使用Java的過(guò)程中,我個(gè)人感覺(jué)有以下幾個(gè)痛點(diǎn):
- 沒(méi)有協(xié)程,無(wú)法輕量的異步。
- 也正是沒(méi)有協(xié)程,IO請(qǐng)求的阻塞,會(huì)導(dǎo)致線(xiàn)程上下文的切換,成本太高。
- 內(nèi)存占用過(guò)高
- 沒(méi)有Context,調(diào)用方請(qǐng)求取消,感知不到;調(diào)用別人的時(shí)候,也沒(méi)有辦法很好的傳遞調(diào)用狀態(tài)及請(qǐng)求取消。