JVM如何優(yōu)雅地分對象?揭秘HotSpot并發(fā)分配的神操作!
前言:線程一多,問題就多
那是一個平凡的工作日,我正打算下樓拿奶茶,剛要起身,就聽隔壁小王爆出一聲吼:
“我這代碼怎么突然OOM了?不是用了并發(fā)隊列嗎?”
我探過頭一看,小王的眼睛都快盯穿了堆棧日志。線程一多,對象分配就開始翻車,不是內(nèi)存亂飛,就是崩潰卡死。我們立刻意識到,幕后那個“對象分配”的黑箱得好好扒一扒了。
于是,這杯奶茶我喝得格外認(rèn)真,邊喝邊翻 JVM 的源碼,一段 HotSpot 虛擬機中處理并發(fā)對象分配的“玄機”悄然浮現(xiàn)——沒錯,我們今天要聊的主角,是 JVM 如何在高并發(fā)環(huán)境下高效且安全地分配對象內(nèi)存的秘密。
圖片
對象是怎么“誕生”的?
在 Java 世界里,萬物皆對象。每當(dāng)我們執(zhí)行一句 new 操作,就相當(dāng)于按下了“制造”按鈕,JVM 背后的內(nèi)存管理機制就開始運行:
- 首先要在堆中劃出一塊連續(xù)的內(nèi)存空間;
- 接著對這塊空間進(jìn)行對象頭填充、字段初始化;
- 最后把這塊“對象胚胎”交到程序手中,完成生命周期的起點。
這個過程看似簡單,但你想啊——如果上百個線程同時“點單”,這工廠還不得擠爆了?
所以,HotSpot 虛擬機到底怎么解決這個問題的呢?
謎底就是我們今天要講的兩大核心策略:
- 同步處理:用 CAS + 失敗重試 來實現(xiàn)原子分配
- 本地線程分配緩沖:每個線程分自己的一攤,減少鎖競爭
第一招:同步處理,CAS+失敗重試,穩(wěn)了!
在沒有 TLAB 的情況下,所有線程都要從一個“共享堆空間”里劃內(nèi)存。這個時候,“搶地盤”就成了問題。
JVM 沒有選擇加鎖,而是更優(yōu)雅地采用了 CAS(Compare-And-Swap)+ 自旋重試 的策略。
1、什么是 CAS?
CAS 是一種原子操作,允許你去比較一塊內(nèi)存的當(dāng)前值是否是你期望的,如果是就更新成新值。
比如現(xiàn)在堆的分配指針指向地址 0x1000,線程 A 想分配 100 字節(jié),它就用 CAS 嘗試把指針更新為 0x1064,如果成功,它就“占坑成功”,分配搞定。
如果失敗,說明別的線程搶先一步改了分配指針,那線程 A 就重新來一遍。
2、CAS 的優(yōu)勢:
- 無需加鎖,性能高;
- 是硬件級別支持的原子操作;
- 多線程下仍然能保證數(shù)據(jù)一致性。
HotSpot 正是基于這種機制來讓對象分配“線程安全”,避免你爭我奪亂成一鍋粥。
不過,這種方式畢竟是所有線程在一個“大鍋”里搶飯吃,線程越多,CAS 失敗率就越高,效率會下降。
于是,JVM 又出了第二招。
第二招:TLAB,劃地為營,各吃各的!
還記得小時候大家搶糖吃嗎?如果所有糖果放在一個盤子里,肯定雞飛狗跳。但如果每個人發(fā)一個小袋子,就能自己慢慢吃,互不打擾。
這就是 TLAB(Thread Local Allocation Buffer)思路的靈感:
JVM 給每個線程分配一個“小堆”,線程創(chuàng)建對象時,優(yōu)先從自己的 TLAB 分配。
是不是特別熟悉?這就好比線程版的“私房錢”。
1、為什么要用 TLAB?
- 避免線程之間的同步?jīng)_突,自己地盤自己用,爽!
- 小對象頻繁創(chuàng)建也不會頻繁競爭鎖,極大提高性能;
- TLAB 是在線程創(chuàng)建時初始化的,不影響其他線程的內(nèi)存分配。
當(dāng)線程使用 new 創(chuàng)建對象時,JVM 會先判斷當(dāng)前線程的 TLAB 是否還有足夠空間:
- 有空間: 直接從 TLAB 中分配,對象創(chuàng)建快如閃電。
- 沒空間: 就申請新的 TLAB,如果申請失敗了才回到老辦法——用 CAS 從全局堆中分配。
2、TLAB 的大小是固定的嗎?
不是。TLAB 的大小一般由 JVM 根據(jù)堆總大小、線程數(shù)、GC 頻率等動態(tài)調(diào)整,而且也可以通過參數(shù)手動干預(yù)。
比如你可以在啟動參數(shù)中控制是否啟用 TLAB:
- -XX:+UseTLAB # 啟用(默認(rèn)開啟)
- -XX:-UseTLAB # 禁用
還能設(shè)置分配比例等細(xì)節(jié):
- -XX:TLABSize=512k
- -XX:+ResizeTLAB
3、TLAB 用完了怎么辦?
TLAB 可不是無限大。一旦當(dāng)前 TLAB 用完,線程會嘗試向堆申請一個新的 TLAB。這個時候才會觸發(fā)同步機制,可能導(dǎo)致暫停、GC 或老年代分配。
所以說,TLAB 是一個優(yōu)化策略,不是銀彈。它非常適合“短平快”的對象,比如業(yè)務(wù)中頻繁構(gòu)建的臨時對象、包裝類等。
模擬生活:對象分配的快與慢
為了讓你更有畫面感,我們模擬一下現(xiàn)實場景:
有一個奶茶工廠,老板(JVM)分給每個員工(線程)一塊原料池(TLAB),大家自己打奶茶,不用排隊。只有當(dāng)原料池見底了,員工才去找倉庫經(jīng)理(堆)申請新原料(新 TLAB)。
如果這個工廠一天要出 10000 杯奶茶,光靠一個倉庫經(jīng)理肯定手忙腳亂。但每個員工有一小桶原料,效率就高了。
TLAB 就像是讓線程各自“帶著干糧上工”,分?jǐn)偭藟毫Α?/p>
深入理解:TLAB + CAS 是搭配來的
可能你注意到了一個細(xì)節(jié):哪怕使用了 TLAB,也還是會有 CAS 的影子。
那是因為創(chuàng)建新 TLAB 本身是從堆中分配空間,這個動作還是要用 CAS 保證線程安全。但因為這個過程不是每次 new 都觸發(fā),所以平均下來效率非常高。
換句話說:
- 平時靠 TLAB,自己吃飯;
- 用完再找 JVM,JVM 用 CAS 給你分新地盤。
這才是 HotSpot 的“并發(fā)分配兩步走”策略:分而治之 + 最小化同步。
TLAB 的開關(guān)和調(diào)優(yōu)姿勢
TLAB 并不是強制開啟的,你可以通過啟動參數(shù)控制它:
- -XX:+UseTLAB # 啟用 TLAB(默認(rèn))
- -XX:-UseTLAB # 禁用 TLAB
- -XX:+PrintTLAB # 打印 TLAB 使用情況
如果你想觀察 TLAB 的命中率、利用率,可以開啟診斷命令:
- jstat -gcutil PID 1000 5
還可以在 JFR(Java Flight Recorder)中看到 TLAB 的命中分析,精確到線程級別。
尾聲:對象分配的智慧
JVM 世界里,對象分配看似一個“小動作”,卻蘊藏了大智慧。
從一開始的 CAS 原子操作,到后來的 TLAB 線程劃分策略,再到按需同步、動態(tài)調(diào)優(yōu),HotSpot JVM 已經(jīng)把對象創(chuàng)建的“并發(fā)沖突”壓縮到了最低。
而你寫下的每一個 new,背后都經(jīng)歷了精妙的內(nèi)存布局計算、線程調(diào)度邏輯,以及無數(shù) JVM 工程師的血淚優(yōu)化。
想想看,我們是不是應(yīng)該對 JVM 稍微多一點敬意?
下次你再喝奶茶的時候,不妨想象一下那一杯“對象”,是不是正是 JVM 高效調(diào)度出來的一杯熱奶香濃呢?




























