小紅書JDK升級(jí)帶來10%整體性能提升,這份升級(jí)指南收好了!

作為年滿 30 歲的“老牌編程語言”,Java 在 GC、JIT、核心庫上持續(xù)迭代進(jìn)化,ZGC、虛擬線程等新特性讓Java開發(fā)者心向往之。然而,升級(jí) JDK 的難度和風(fēng)險(xiǎn)卻讓眾多開發(fā)者望而卻步——想升級(jí)享受高性能,又怕牽一發(fā)動(dòng)全身?如何才能緊追社區(qū)版本,享受 JDK 技術(shù)紅利又避免踩坑?小紅書用實(shí)戰(zhàn)給出答案。小紅書中間件團(tuán)隊(duì)在過去一年時(shí)間支持小紅書 Java 業(yè)務(wù)從 JDK8 大規(guī)模遷移到 RedJDK11 或 RedJDK17,在復(fù)雜的業(yè)務(wù)場景和工程環(huán)境下,通過技術(shù)手段高效、穩(wěn)定完成了整體 JDK 架構(gòu)升級(jí),最終拿到整體 10%+ 的性能收益,GC 開銷下降 50%。基本消除 Java 服務(wù) oom、crash 等穩(wěn)定性風(fēng)險(xiǎn),成功通過兩次春節(jié)活動(dòng)和 618 大促的實(shí)戰(zhàn)檢驗(yàn)。
01、升級(jí)背景
1.0 總體背景
《TIOBE 編程語言排行》2025 年 6 月報(bào)告出爐,Python、C/C++、Java 仍然穩(wěn)居前列,它們特點(diǎn)不同,在各自最適合的使用場景難以替代——例如 Python 在 AI 領(lǐng)域及 C/C++ 在游戲開發(fā)場景的地位。Java 依靠其豐富的生態(tài)和強(qiáng)大的 JVM 支持,成為了企業(yè)級(jí)后端應(yīng)用和大數(shù)據(jù)處理的絕佳選擇。同時(shí),Java 在小紅書后端技術(shù)體系中扮演重要支撐角色。
近年來,小紅書增長迅猛,搜索、推薦、廣告、電商等核心業(yè)務(wù)負(fù)載居高不下,對(duì) JVM 穩(wěn)定性、服務(wù)運(yùn)行效率和 GC 卡頓時(shí)長提出嚴(yán)格要求——JVM Crash 和 GC 秒級(jí)卡頓行為足以威脅生產(chǎn);同時(shí),Spring、Flink、Spark等框架陸續(xù)棄用 Java8。在社區(qū)大發(fā)展環(huán)境下,JDK8 的性能、穩(wěn)定性和對(duì)業(yè)務(wù)的支撐已難以滿足小紅書當(dāng)前階段訴求,貌似必須掌控 JDK 靈活升級(jí)的辦法,才能在未來高速變化的環(huán)境中更好地支持業(yè)務(wù)迭代。
1.1 價(jià)值驅(qū)動(dòng)
伴隨著小紅書業(yè)務(wù)來到更高的發(fā)展階段,對(duì)后端服務(wù)的性能與穩(wěn)定性提出了嚴(yán)格要求,然而 JDK 作為 Java 服務(wù)的底座,卻暴露出很多問題與風(fēng)險(xiǎn),需要系統(tǒng)性解決。小紅書通過統(tǒng)一管控JDK產(chǎn)品生態(tài)并升級(jí)到 JDK11,可以從成本、穩(wěn)定和標(biāo)準(zhǔn)化多方面直接或間接獲得巨大的收益。
- 直接成本收益:承接同等的請(qǐng)求需要的CPU降低,進(jìn)一步可以進(jìn)行縮容和退機(jī),轉(zhuǎn)換成直接成本收益
 - 間接成本受益:隨著業(yè)務(wù)不斷發(fā)展,未來增長的 cpu 申請(qǐng)quota也會(huì)隨著優(yōu)化變少
 - 業(yè)務(wù)發(fā)展需要:眾多新一代主流開源項(xiàng)目(如 Spring Boot 3.x、Kafka 4.0 等)已陸續(xù)停止 JDK8 支持
 - 穩(wěn)定性收益:高版本 JDK 修復(fù)了諸多 GC、JIT、Corelibs 中的 Bugs,消除潛在穩(wěn)定性風(fēng)險(xiǎn)
 - 運(yùn)維收益:統(tǒng)一使用獨(dú)立開發(fā)維護(hù)的JDK版本,靈活根據(jù)業(yè)務(wù)需要提供適配支持;標(biāo)準(zhǔn)化使用姿勢(shì)減少業(yè)務(wù)運(yùn)維負(fù)擔(dān)
 

1.2 價(jià)值體現(xiàn)
1.2.1 G1GC 優(yōu)化提高性能 & 穩(wěn)定性
G1GC 于 JDK7 開始被支持,隨后在 JDK9 中才成為默認(rèn)的 GC 算法,其算法核心特點(diǎn)是分代、基于預(yù)測(cè)單次停頓時(shí)長控制 GC 行為。小紅書的在線應(yīng)用注重用戶及時(shí)交互體驗(yàn),因此對(duì)響應(yīng)延時(shí)指標(biāo)極其看重,在這樣的業(yè)務(wù)場景下,G1GC 成為了小紅書的首選 GC 算法。
然而,我們發(fā)現(xiàn) JDK8 中實(shí)現(xiàn)的 G1GC 仍存在不少缺陷,其不準(zhǔn)確的預(yù)測(cè)模型和并發(fā)回收特性造成了額外的資源開銷,甚至對(duì)生產(chǎn)穩(wěn)定性產(chǎn)生威脅。通過升級(jí) JDK,可以顯著提高 GC 吞吐,降低 RT。
G1GC 優(yōu)化 —— 并行 FullGC
在小紅書很多核心場景都會(huì)偶發(fā) FullGC 問題,尤其是在大型活動(dòng)時(shí)問題更為頻繁,F(xiàn)ullGC 通常導(dǎo)致秒級(jí)以上的卡頓。如果是JDK8,不論CPU資源多充足,F(xiàn)ullGC 都只能通過單線程串行完成。G1GC 在JDK10(JEP-307)中提出通過多線程并行完成 FullGC,大大降低 FullGC 造成的負(fù)面影響。
G1GC 優(yōu)化 —— 靜態(tài)的 IHOP
IHOP 全稱 InitiatingHeapOccupancyPercent,是控制 G1GC 觸發(fā)并發(fā)標(biāo)記的關(guān)鍵參數(shù)。默認(rèn)情況下,當(dāng) Old 區(qū)大小超過全堆內(nèi)存的 45%,G1 會(huì)開啟一個(gè)與 Mutators 并發(fā)進(jìn)行的標(biāo)記階段,用來回收老年代內(nèi)存。如圖所示,當(dāng)開始并發(fā)回收的時(shí)候 (Concurrent Start),mutator 行為并不會(huì)停止——這避免了長時(shí)間的停頓,但要求 G1GC 盡快完成 GC,避免堆內(nèi)存打滿導(dǎo)致的 FullGC 行為。
- 并發(fā)標(biāo)記開始于老年代內(nèi)存占用 45% 時(shí)(參數(shù)InitiatingHeapOccupancyPercent)
 - Young 區(qū)內(nèi)存占比最高可以達(dá)到全堆的 60%(參數(shù)G1MaxNewSizePercent);
 - 并發(fā)標(biāo)記階段,應(yīng)用仍在持續(xù)分配新內(nèi)存,部分晉升到老年代(實(shí)際promotion_rate);
 - 為了處理晉升毛刺,G1 應(yīng)預(yù)留 10% 的內(nèi)存(參數(shù)G1ReservePercent);
 
由此計(jì)算,堆內(nèi)存總是不夠的!實(shí)際上,未經(jīng)調(diào)優(yōu)的 JDK8 G1GC 長期暴露在 FullGC 的風(fēng)險(xiǎn)下。

G1GC 優(yōu)化 —— 提前回收大對(duì)象(Humongous)
G1 是一種 Region-Based GC 算法,將整個(gè) Heap 分為若干等大的內(nèi)存塊。如果一個(gè) Java 對(duì)象,占用內(nèi)存超過 G1 Region Size的一半,那么他將會(huì)獨(dú)占一個(gè)或若干連續(xù)的 G1 Region,這種對(duì)象叫做大對(duì)象(Humongous)。大對(duì)象具備以下特點(diǎn):
- 大對(duì)象直接分配在 Empty Region,不會(huì)被 Copy,GC 回收一個(gè)大對(duì)象基本沒有開銷;
 - 大對(duì)象每個(gè) Region 只有一個(gè)對(duì)象,Rset 維護(hù)代價(jià)極低;
 - 回收一個(gè)大對(duì)象可以直接清理出若干完整的 G1 Region,ROI 高!
 
因此,在 JDK9+ 版本,G1GC 選擇在 Young-Only GCs 中直接回收大對(duì)象,大大減輕了 GC 的開銷和內(nèi)存壓力!
G1GC Bug —— 錯(cuò)誤的 rs_length 預(yù)估
G1 的算法核心是:通過調(diào)節(jié)各分區(qū)內(nèi)存大小,控制GC觸發(fā)時(shí)機(jī),保障STW時(shí)間控制在一定時(shí)間內(nèi)(默認(rèn)200ms)。那么G1如何預(yù)測(cè)接下來的 GC 卡頓時(shí)長呢?
答案就是將歷史的 GC 情況代入一個(gè)衰減偏差模型中,判斷在規(guī)定時(shí)間內(nèi)能完成多少內(nèi)存的回收工作,衰減模型會(huì)綜合考慮過去一段時(shí)間區(qū)間內(nèi)的數(shù)據(jù)。具體路徑為:
- 根據(jù)歷史數(shù)據(jù)得到預(yù)測(cè)數(shù)據(jù);
 - 根據(jù)預(yù)測(cè)數(shù)據(jù)和目標(biāo)停頓時(shí)長,決策GC觸發(fā)時(shí)機(jī);
 
在預(yù)測(cè)的過程中,RSet 大小的記錄與預(yù)測(cè)非常關(guān)鍵,該 Bug 是 GC 根據(jù)歷史數(shù)據(jù)預(yù)測(cè)不準(zhǔn)確,導(dǎo)致影響 GC 行為的一個(gè)典例,可以看看 JDK 部分源碼:

預(yù)測(cè)的base_time_ms強(qiáng)依賴于 predict_rs_lengths。于是問題就發(fā)生了,實(shí)際情況如圖所示:
- 在Mixed GC開始時(shí),下圖藍(lán)線所示 Mixed GC 期間 Rset 的大小會(huì)突然增加(t1時(shí)),紅線所示對(duì)RSet大小預(yù)測(cè)值開始逐漸上升,但由于歷史值影響,預(yù)測(cè)仍小于實(shí)際值,這導(dǎo)致 GC stw 時(shí)長超過目標(biāo)值;
 - MixedGC 結(jié)束后,Rset 突然變小,預(yù)測(cè)值又會(huì)高于實(shí)際 Rset length。G1 沒有考慮 MixedGC 的發(fā)生,單純基于自己預(yù)測(cè)數(shù)據(jù)仍然認(rèn)為 rset 很大,因此根據(jù)算法保持 Eden 區(qū)在很小的空間,從而帶來了GC次數(shù)的陡增。
 

1.2.2 JVM Bug 在高版本修復(fù)
Bug——ReentrantLock 在遇到 StackOverflowError 時(shí)無法釋放
ReentrantLock 是 Java 并發(fā)包中提供的顯式鎖機(jī)制,使用 ReentrantLock 時(shí),需要在 finally 塊中主動(dòng) unlock,避免死鎖。然而,如果這里因?yàn)檫f歸等原因?qū)е?StackOverflowError,則 finally { lock.unlock(); } 無法正常執(zhí)行,最終可能致使整個(gè) Java 進(jìn)程死鎖。

Bug —— Heapdump / AGCT 等行為導(dǎo)致 Crash
火焰圖和 Heapdump 是分析 Java 線上問題的常用工具,然而在我們實(shí)際使用過程中,類似工具會(huì)有較大概率導(dǎo)致 JVM Crash,對(duì)線上服務(wù)產(chǎn)生影響。其中原因種種,大多可以在 JBS(OpenJDK Bug System)中找到對(duì)應(yīng)缺陷,需要通過升級(jí) JDK 解決。
1.2.3 更強(qiáng)大的 VM 特性
Pauseless GC 家族 —— ZGC 與 ShenandoahGC
G1GC 通過并發(fā)標(biāo)記和 SATB 算法,大幅降低了 GC 造成的卡頓,但 Java 應(yīng)用仍然需要等待數(shù)十至數(shù)百毫秒的卡頓。新一代的 Pauseless GC 通過將更多 GC 工作放在并發(fā)階段完成,成功進(jìn)一步降低了 GC 卡頓時(shí)間。Pauseless GC 最早是由 Azul 公司實(shí)現(xiàn)的 C4GC,在商業(yè)版JDK產(chǎn)品——Zing 中提供支持。隨后 Oracle 和 Redhat 分別實(shí)現(xiàn)了 ZGC 和 ShenandoahGC,OpenJDK 中支持使用。

- JDK11 支持試用 ZGC(JEP-333)和 ShenandoahGC(JEP-189),控制 GC Pause 小于 10ms;
 - 在 JDK16+ ZGC 和 ShenandoahGC 宣布生產(chǎn)可用,GC Pause 逐漸優(yōu)化到 1ms 內(nèi);
 - 在 JDK21、JDK25 分別支持分代的 ZGC、ShenandoahGC,在個(gè)別場景GC 的吞吐可以看到 80% 的提升;
 

支持 String Compact(JEP-254)
在 Java 的世界中,一個(gè) 'char' 需要占用兩個(gè)字節(jié),即 16 bits,而 ASCII 字符的編碼位于 0-127 之間,即僅需要 8 bits,通過 'char[]' 類型存儲(chǔ)英文字符串將會(huì)導(dǎo)致一倍的內(nèi)存浪費(fèi)。JDK9+ 默認(rèn)開啟 String Compact,對(duì) java.lang.string 類添加了一個(gè) coder 字段,快速判斷該 String 是否被 Compact。其作用是為了在部分場景降低一倍的 String 內(nèi)存占用(及對(duì)應(yīng) String 操作開銷),如下圖所示:

1.3 挑戰(zhàn)與風(fēng)險(xiǎn)
上文提到的各種優(yōu)化特性預(yù)期為 Java 應(yīng)用帶來巨大優(yōu)化,但要將小紅書幾千個(gè) Java 服務(wù)統(tǒng)一完成 JDK 升級(jí)仍有不小難度,需要妥善考慮各種情況。
兼容性風(fēng)險(xiǎn)
- Project Jigsaw 模塊化與權(quán)限管控:由于 Project jigsaw 引入了模塊系統(tǒng)重構(gòu)并收緊了訪問權(quán)限,很多被JDK8允許的Java代碼并不能直接運(yùn)行在更高版本的JDK上;
 - Compact String 導(dǎo)致 String 結(jié)構(gòu)變化:String Compact 是 JDK9 引入的一個(gè)非常好的優(yōu)化特性,但使用不當(dāng)可能導(dǎo)致序列化/反序列化失敗,或序列化庫開銷暴增;
 - JDK 默認(rèn)行為變化:高版本 JDK 在TLS 協(xié)議、Locale 支持等行為默認(rèn)發(fā)生了變化,可能產(chǎn)生兼容性問題。另外,部分標(biāo)準(zhǔn)庫的行為發(fā)生了改變(比如之后 Hashmap.computeIfAbsent() 可能拋出 ConcurrentModificationException),影響并改變?cè)緲I(yè)務(wù)處理邏輯;
 - JVM運(yùn)行參數(shù)變化:GC 參數(shù)、容器化參數(shù)、日志配置方式均發(fā)生變化,錯(cuò)誤配置可能導(dǎo)致內(nèi)存上漲、性能回退,甚至導(dǎo)致 JVM 退出;
 - 依賴復(fù)雜:上述兼容性問題同樣可能發(fā)生在依賴的二方/三方包中,使用高版本 JDK 運(yùn)行需要替換升級(jí)到支持高版本 JDK 的依賴包,或通過其他手段克服解決。
 
升級(jí)推進(jìn)風(fēng)險(xiǎn)
- 需要配套流程支持:在數(shù)千Java應(yīng)用升級(jí)JDK,需要考慮底層鏡像、容器環(huán)境異構(gòu),和構(gòu)建/部署流程與 JDK 升級(jí)帶來的沖突;
 - 需多團(tuán)隊(duì)配合完成:JDK 升級(jí)過程需要基礎(chǔ)平臺(tái)、構(gòu)建/部署/測(cè)試平臺(tái)、Java業(yè)務(wù)方等多團(tuán)隊(duì)共同參與,對(duì)組織橫向協(xié)同和流程治理能力是一個(gè)巨大挑戰(zhàn)。;
 - 部分邊緣Case排查:遇到業(yè)務(wù)行為不符合預(yù)期或性能退化的情況,需要對(duì)問題有清晰的排查和解釋,避免盲目升級(jí)帶來的額外風(fēng)險(xiǎn)。
 
1.4 項(xiàng)目目標(biāo)
性能收益
推動(dòng)小紅書 Java RPC 服務(wù)、大數(shù)據(jù)業(yè)務(wù)、中臺(tái)服務(wù)統(tǒng)一升級(jí)到 RedJDK11/RedJDK17,取得平均 10% 的性能提高與成本收益。
清除歷史債務(wù)
通過升級(jí)項(xiàng)目,完成小紅書Java服務(wù)的環(huán)境治理,管控并統(tǒng)一底層鏡像、JDK 產(chǎn)品、構(gòu)建流程和 Java 運(yùn)行參數(shù);
解決當(dāng)前頻發(fā)的 (因JDK引起的)OOM、GC抖動(dòng)、CPU 異常,Java服務(wù)穩(wěn)定性有明顯提高;
通過能力建設(shè)解決 GC 毛刺、Native 內(nèi)存泄露等棘手問題無趁手工具排查的窘境。
穩(wěn)定保障
升級(jí)過程中,不出現(xiàn)P3以上的穩(wěn)定性問題。
02、JDK升級(jí)方案
為了解決在復(fù)雜生產(chǎn)環(huán)境升級(jí) JDK 產(chǎn)生的諸多難點(diǎn),小紅書通過 JDK 源碼改造和環(huán)境/流程標(biāo)準(zhǔn)化等手段,屏蔽掉了JDK 底層行為變化帶來的風(fēng)險(xiǎn),對(duì)于 Java 開發(fā)同學(xué)無需關(guān)注 JDK 內(nèi)部過多細(xì)節(jié),即可享受最新的語言特性和運(yùn)行時(shí)能力!為業(yè)務(wù)同學(xué)節(jié)省大量操作成本和心理負(fù)擔(dān)。
小紅書中間件團(tuán)隊(duì)采用“JDK 兼容性改造——標(biāo)準(zhǔn)化執(zhí)行——特例針對(duì)性升級(jí)”的模式,即將風(fēng)險(xiǎn)控制在上線前,快速完成大部分場景的遷移升級(jí)工作,篩查出特殊場景并通過技術(shù)手段收口,最終實(shí)現(xiàn)全業(yè)務(wù)升級(jí)遷移到 JDK11 / 17 / 21 的技術(shù)目標(biāo)。
2.1 具體問題與策略
可以在JDK側(cè)統(tǒng)一解決的兼容問題

Compact String 導(dǎo)致 String 結(jié)構(gòu)變化
String Compact 是 JDK9 引入的一個(gè)非常好的優(yōu)化特性,但使用不當(dāng)可能導(dǎo)致最危險(xiǎn)的 Bad Case。從JDK8或更低版本升級(jí),需要謹(jǐn)慎考慮:
- 如果采用無 IDL 的序列化算法處理 String(如 protostuff),并使用普通的 RuntimeSchema 序列化 String 對(duì)象,會(huì)因?yàn)?String 結(jié)構(gòu)變化導(dǎo)致序列化/反序列化結(jié)果不一致。需注意:務(wù)必規(guī)范使用 String 專用的 Schema;
 - Compacted String 如果需要拼接非 Latin 字符,則整個(gè) String 需要膨脹回 UTF-16 格式存儲(chǔ),在中/英字符串拼接場景可能開銷更大;
 
2.2 優(yōu)化改造
在高并發(fā)、高負(fù)載的生產(chǎn)環(huán)境中,JVM 的 GC 表現(xiàn)往往成為系統(tǒng)穩(wěn)定性的"阿喀琉斯之踵"。我們發(fā)現(xiàn)小紅書存在以下三類典型問題亟需系統(tǒng)性解決:
- GC卡頓毛刺:YoungGC表現(xiàn)不穩(wěn)定,在晚高峰可能迎來 GC 開銷陡增。
 - 定時(shí)炸彈FullGC:即使是低負(fù)載實(shí)例,也可能偶發(fā) FullGC 造成嚴(yán)重可用性問題。
 - 內(nèi)存失控:Java 服務(wù)普遍面臨著 native 內(nèi)存分配器的內(nèi)存泄漏問題與高 OOM 風(fēng)險(xiǎn),依賴定期重啟緩解。
 
2.2.1參數(shù)優(yōu)化
- 通過 G1GC 參數(shù)調(diào)優(yōu),解決 G1 在復(fù)雜場景下造成的抖動(dòng)問題:
 
MaxGCPauseMillis / G1NewSizePercent / InitiatingHeapOccupancyPercent / G1UseAdaptiveIHOP / G1ReservePercent
- 標(biāo)準(zhǔn)化內(nèi)存分配參數(shù),統(tǒng)一Java運(yùn)行規(guī)格,便利運(yùn)維工作:
 
InitialRAMPercentage / MaxRAMPercentage / ReservedCodeCacheSize / MaxDirectMemorySize
- 基于歷史經(jīng)驗(yàn),選擇性打開或關(guān)閉部分運(yùn)行時(shí)特性,得到最佳性能表現(xiàn):
 
打開 ParallelRefProcEnabled,關(guān)閉 UseStringDeduplication 與 UseBiasedLocking;
2.2.2 深度源碼改造
FullGC 引發(fā)劇烈 GC 抖動(dòng)
小紅書普遍使用 G1 GC 算法,G1 會(huì)在 Full GC 后,以當(dāng)前內(nèi)存使用量為基準(zhǔn)重新計(jì)算 Heap 大小,這個(gè)行為往往會(huì)造成堆大小減少以及 Young 區(qū)減少,進(jìn)一步造成 GC 頻率加快、開銷陡增。由于小紅書業(yè)務(wù)普遍對(duì) RT 敏感,且 Shrink 掉的 Memory 實(shí)際上并不能得到利用,因此 RedJDK 默認(rèn)使用穩(wěn)定的 Java Heap,減少不必要的內(nèi)存分配/歸還。

JEP-358 helpful NPE
通常我們遇到空指針異常,會(huì)得到一條非常簡單的NPE報(bào)錯(cuò),它表示訪問了某個(gè)對(duì)象為空:
Exception in thread "main"
java.lang.NullPointerException
通過在 RedJDK11 支持 JEP-358,NPE 將直接告訴你,哪一行、哪個(gè)引用為空!是否只有開發(fā)同學(xué)才能理解,看到報(bào)錯(cuò)信息后立刻恍然大悟的那種救贖感。

StringBuilder性能優(yōu)化
在升級(jí) JDK11 時(shí),發(fā)現(xiàn)部分中英文混雜場景下 StringBuilder 字符串處理能力存在性能劣化,原因可歸結(jié)為以下幾點(diǎn):
- JDK11 StringBuilder 在 append 中英文混合字符串時(shí)存在性能瓶頸。默認(rèn) Latin 編碼遇到非 Latin 字符需要判斷和擴(kuò)容,且通過 putChars 逐字符拷貝效率較低。RedJDK11 通過 backport JDK-8224986,JDK-8273100 等 patch,實(shí)現(xiàn)基于 System.arraycopy 的高性能 append(String s, int start, int end) 方法,可提升非 Latin 字符 append 性能。
 - StringBuilder 的 UTF16 的 toStirng() 等方法需要通過遍歷檢查 byte[] 數(shù)組中是否存在非 Latin 字符,來判斷是否可以處理為 UTF8。RedJDK11 通過 backport JDK-8282429 等 patch,在 AbstractStringBuilder 增刪改的時(shí)候提前記錄是否可能存在非 Latin 字符,避免后續(xù)對(duì) byte[] 重復(fù)遍歷掃描。
 
例如,在 GSON.toJson() 時(shí),會(huì)調(diào)用 StringBuilder.append() 與 StringBuilder.toString() 方法,通過上述優(yōu)化,可提高此方法在中英文混雜場景下的性能。
Scoped Heapdump
Java 大多數(shù)對(duì)象是符合“朝生夕死”的分代假說原理,G1GC 將長期存活的對(duì)象放在 Old 區(qū),將短期存活的對(duì)象放在 Eden 區(qū),僅有少部分對(duì)象需要實(shí)際在 STW 中拷貝。這部分對(duì)象的大小直接影響了 G1 YoungGC 的耗時(shí),然而在傳統(tǒng) Heapdump 手段中,Eden、Old 區(qū)的對(duì)象將對(duì)我們的分析產(chǎn)生極大干擾,影響我們找到影響 YGC 的元兇。
RedJDK11 對(duì) Jmap 進(jìn)行了改造,可以指定 Dump 任意分區(qū)的對(duì)象 ,例如 scope=g1_survivor,再通過 MAT、Jifa 等工具分析即可。

2.2.3 Native內(nèi)存治理
作為新一代 Malloc 實(shí)現(xiàn),Jemalloc 完成了虛擬地址空間和物理內(nèi)存的解構(gòu)設(shè)計(jì),持有大量的虛擬內(nèi)存空間,減少系統(tǒng)調(diào)用的同時(shí),Jemalloc 通過一系列設(shè)計(jì)防止物理內(nèi)存的浪費(fèi),定期向操作系統(tǒng)歸還過多緩存的物理內(nèi)存:
- Jemalloc 以 4K 為內(nèi)存管理的基本單元(Extent)——遠(yuǎn)小于Glibc 的 64M,與OS Page 大小保持一致;
 - 所有用戶分配會(huì)被向上取整到最接近的 alloc SizeClass,這些預(yù)設(shè)的 SizeClass 可以大幅減少內(nèi)存塊碎片化程度;
 - 當(dāng)一個(gè) Extent 的內(nèi)存全部被用戶釋放,將 Extent 放入緩存區(qū),等待歸還;
 - Extent 的內(nèi)存全部被用戶釋放會(huì)進(jìn)入緩存區(qū),在還給操作系統(tǒng)前,可以直接用來滿足新的分配(減少進(jìn)入內(nèi)核態(tài)的頻率);
 - 經(jīng)過一段時(shí)間仍未重復(fù)利用的Extent將歸還其物理內(nèi)存,緩存其虛擬內(nèi)存(減少系統(tǒng)調(diào)用頻率);這個(gè)頻率可以通過參數(shù) dirty_decay_ms 和 muzzy_decay_ms 配置,默認(rèn)為10s;
 

采用 Jemalloc 替代 Ptmalloc 后實(shí)現(xiàn):
- 內(nèi)存碎片化控制:每個(gè)內(nèi)核頁(4K Bytes)只用于分配固定 Size 的內(nèi)存,內(nèi)存碎片問題得到解決。
 - 內(nèi)存回收機(jī)制:通過定時(shí)器和 malloc 活躍度及時(shí)將內(nèi)存歸還給 OS,控制 RSS 水位貼合應(yīng)用實(shí)際需求。
 - 泄漏診斷能力:支持 jeprof、malloc_stat_print、mallctl 等工具,可以分析 JVM 可見或不可見的 Native 內(nèi)存,比 NMT 工具更全面、輕量。
 - 低線程競爭開銷:在 Jemalloc Arena 內(nèi),支持 Size Bin 粒度鎖,多線程并發(fā)分配效率大大提高。
 
2.3 依賴版本升級(jí)
對(duì)于升級(jí) JDK 的 Java 應(yīng)用,需要篩查下依賴版本能否兼容 JDK11。Java 生態(tài)豐富,難以列舉所有依賴版本。根據(jù)小紅書的經(jīng)驗(yàn),這些依賴需要重點(diǎn)關(guān)注:

2.4 打包、測(cè)試、上線
完成上述工作,就可以打包上線了。在平臺(tái)組支持下,小紅書得以實(shí)現(xiàn)一整套構(gòu)建、部署標(biāo)準(zhǔn)化流程,支持從 JDK8 的構(gòu)建部署流程“一鍵切換”到 JDK11,并完成鏡像、制品、JDK參數(shù)的管控。升級(jí)過程中業(yè)務(wù)團(tuán)隊(duì)僅需要少數(shù)操作與觀察,即可平穩(wěn)完成 JDK 升級(jí)。
03、成果總結(jié)
3.1 RT變化
部分服務(wù)使用 JDK8 時(shí) GC 卡頓嚴(yán)重,升級(jí)到 RedJDK11 后能看到 P99RT 明顯下降,毛刺基本消除。下圖為某核心場景晚高峰 RT 指標(biāo),黃線代表使用 JDK8 時(shí)晚高峰時(shí)段的 P99 RT,綠線為升級(jí) RedJDK11 后的指標(biāo):

3.2 CPU使用率變化
CPU水位同樣變化明顯,通過升級(jí) RedJDK11,許多服務(wù)能明顯觀測(cè)到 CPU 使用率水位下降,根據(jù)數(shù)百服務(wù)指標(biāo)分析,全局平均可以取得 10% 的性能收益。

3.3 內(nèi)存使用率變化
通過切換使用 Jemalloc Native 內(nèi)存分配器,部分服務(wù)可以得到顯著降低。且不再出現(xiàn)內(nèi)存持續(xù)爬升上漲的問題。

3.4 總體成果
在各合作團(tuán)隊(duì)的支持與緊密配合下,我們成功支持推動(dòng)小紅書百萬 CPU 規(guī)模的 Java 服務(wù)升級(jí)遷移到 RedJDK11,并支持大數(shù)據(jù)業(yè)務(wù)接入使用RedJDK17,規(guī)??蛇_(dá)數(shù)十萬。取得多方面收益:
- CPU:
 
a.在保證性能沒有下降的情況下,平均降低峰值 CPU 利用率 10%。
b.在 Flink 樣本作業(yè)與 Spark 任務(wù)上,分別取得 15% 和 8.6% 的算力提升。
- 穩(wěn)定性:
 
a.GC STW 卡頓降低50%,顯著減少P99 RT;
b.解決GC異常、進(jìn)程內(nèi)存爬升,OOM問題基本消除;
c.暴露出的 JVM Crash 問題基本解決;
- 架構(gòu)提升:
 
a.完成JDK產(chǎn)品管控,在中間件服務(wù)框架和工程效率、發(fā)布平臺(tái)團(tuán)隊(duì)的支持下,業(yè)務(wù)可以一鍵升級(jí)到JDK17、JDK21乃至更高版本JDK。
04、未來展望
今年 AI 無疑是技術(shù)圈最火的話題之一了,Java 業(yè)務(wù)通常通過 SpringAI 框架快速打通 Spring 生態(tài)和 AI 技術(shù)生態(tài)。一方面,為了滿足 Spring Framework 6+ 對(duì) JDK 版本的要求(最低 JDK17),另一方面,我們十分看重 JDK 性能進(jìn)一步提升,以及 Java 虛擬線程(JEP-444)、分代 ZGC(JEP-439)等高級(jí)運(yùn)行時(shí)特性。
為此,小紅書未來計(jì)劃以 OpenJDK21 為新的支持底座,持續(xù)投入建設(shè)小紅書的 RedJDK21 產(chǎn)品,支持 RedJDK21 產(chǎn)品和虛擬線程等技術(shù)在小紅書生產(chǎn)環(huán)境落地,我們期待與業(yè)界分享技術(shù)紅利,共同見證 Java 在當(dāng)前時(shí)代下的機(jī)遇與挑戰(zhàn)。
JDK21 性能測(cè)試
在 SPECjbb2015 基準(zhǔn)測(cè)試中,根據(jù) JDK21 vs JDK11 測(cè)試結(jié)果分析:
- Max-JOPS 指標(biāo):無延遲限制下的最大吞吐量,用于評(píng)估系統(tǒng)極限性能
 - Critical-JOPS 指標(biāo):10ms、25ms、50ms、75ms、100ms下的綜合吞吐量,反應(yīng)生產(chǎn)環(huán)境實(shí)際吞吐性能
 - P99 指標(biāo):在指定固定 P99 latency指標(biāo)下的吞吐量
 


Java 虛擬線程
從 JDK21 開始 Java 虛擬線程特性宣布生產(chǎn)可用,這也是我們升級(jí) JDK21 的一大動(dòng)力。使用虛擬線程可以達(dá)到簡化編程并提高應(yīng)用吞吐的效果,小紅書的 Java 服務(wù)具有請(qǐng)求耗時(shí)較短、網(wǎng)絡(luò)I/O高的特點(diǎn),使用虛擬線程預(yù)期能大幅提高服務(wù)吞吐,更充分得利用 CPU、內(nèi)存等物理資源。
下圖是 Java 虛擬線程調(diào)度示意圖,相比于傳統(tǒng)線程模型中,通過內(nèi)核的調(diào)度能力處理阻塞于運(yùn)行,Java 虛擬線程將任務(wù)的調(diào)度和執(zhí)行放在 JVM 中完成,實(shí)現(xiàn)了 Java 邏輯和 OS 線程的結(jié)耦。當(dāng)任務(wù)發(fā)生阻塞后,JVM 會(huì)妥善保存執(zhí)行上下文,并選擇可運(yùn)行的 Java 任務(wù)開始或恢復(fù)運(yùn)行。在這樣的設(shè)計(jì)下,JVM 只需要?jiǎng)?chuàng)建少量的線程,就可以支持百萬級(jí)并發(fā)任務(wù)的運(yùn)行。
JVM 無法處理部分由平臺(tái) API 導(dǎo)致的阻塞 ,例如文件 I/O、JNI 阻塞以及 Synchronized,可能導(dǎo)致 JVM 處理能力下降。其中 Synchronized 運(yùn)用最為廣泛,從老架構(gòu)遷移基本難以避開。在 OpenJDK24 之后的版本,社區(qū)通過底層重構(gòu)(JEP-491)實(shí)現(xiàn)了虛擬線程對(duì) Synchronized 關(guān)鍵字的支持,JDK24 并非 LTS版本,難以達(dá)到生產(chǎn)環(huán)境使用標(biāo)準(zhǔn)。因此,小紅書決定跟隨社區(qū)的步伐,基于 JDK21 徹底重構(gòu) Synchronized 和 ObjectMonitor的底層實(shí)現(xiàn),支持虛擬線程下的任務(wù)掛起、切換,滿足線上場景應(yīng)用的需要。

05、作者介紹
繼才(楊道談)
小紅書中間件負(fù)責(zé)人,曾就職于快手基礎(chǔ)架構(gòu)團(tuán)隊(duì),目前整體負(fù)責(zé)小紅書中間件的規(guī)劃、研發(fā)和日常維護(hù)工作,負(fù)責(zé)微服務(wù)中間件(RPC、注冊(cè)中心、配置中心、任務(wù)調(diào)度、服務(wù)治理)、消息中間件、JDK Runtime 三個(gè)領(lǐng)域方向工作。
兇真(劉俠麟)
小紅書JDK研發(fā)負(fù)責(zé)人,畢業(yè)于華中科技大學(xué),曾就職于快手JVM團(tuán)隊(duì),目前是小紅書 JDK 團(tuán)隊(duì)負(fù)責(zé)人,負(fù)責(zé) RedJDK 的日常規(guī)劃、研發(fā) 和 迭代工作,推進(jìn) JDK21、ZGC、多租戶、虛擬線程在小紅書落地。
沈浪(呂洪武)
小紅書JDK研發(fā)工程師,畢業(yè)于北京理工大學(xué),校招進(jìn)入小紅書 JDK 團(tuán)隊(duì),目前主要從事 RedJDK17 在 Flink/Spark 的升級(jí)工作,同時(shí)推動(dòng) ZGC 在小紅書的落地。
伊澄(易謙)
小紅書JDK研發(fā)工程師,畢業(yè)于北京大學(xué),校招進(jìn)入小紅書 JDK 團(tuán)隊(duì),目前主要從事 JVM 多租戶在熱部署場景的落地、以及虛擬線程的工作。















 
 
 










 
 
 
 