經常聽到 Java 效能不如 C/C++ 的言論,也經常聽說 Java 程式需要預熱,那麼其中主要原因是啥呢?
面試的時候談到 JVM,也有很多面試官喜歡問,為啥 Java 程式越執行越快呢?
一般人都能回答上來,類載入,快取預熱等等,但是深入下去,最重要的卻沒有答上來,今天本系列文章就來幫助大家理解這個問題的關鍵。本篇文章是 TLAB 預熱。
TLAB(Thread Local Allocation Buffer)執行緒本地分配快取區,這是一個執行緒專用的記憶體分配區域。
既然是一個記憶體分配區域,我們就先要搞清楚 Java 記憶體大概是如何分配的。
我們這裡不考慮棧上分配,這些會在 JIT 的章節詳細分析,我們這裡考慮的是無法棧上分配需要共享的物件。
對於 HotSpot JVM 實現,所有的 GC 演算法的實現都是一種對於堆記憶體的管理,也就是都實現了一種堆的抽象,它們都實現了介面 CollectedHeap。當分配一個物件堆記憶體空間時,在 CollectedHeap 上首先都會檢查是否啟用了 TLAB,如果啟用了,則會嘗試 TLAB 分配;如果當前執行緒的 TLAB 大小足夠,那麼從執行緒當前的 TLAB 中分配;如果不夠,但是當前 TLAB 剩餘空間小於最大浪費空間限制(這是一個動態的值,我們後面會詳細分析),則從堆上(一般是 Eden 區) 重新申請一個新的 TLAB 進行分配。否則,直接在 TLAB 外進行分配。TLAB 外的分配策略,不同的 GC 演算法不同。例如G1:
- 如果是 Humongous 物件(物件在超過 Region 一半大小的時候),直接在 Humongous 區域分配(老年代的連續區域)。
- 根據 Mutator 狀況在當前分配下標的 Region 內分配
這裡,我們先只關心 TLAB 分配。
對於單執行緒應用,每次分配記憶體,會記錄上次分配物件記憶體地址末尾的指標,之後分配物件會從這個指標開始檢索分配。這個機制叫做 bump-the-pointer (撞針)。
對於多執行緒應用來說,記憶體分配需要考慮執行緒安全。最直接的想法就是通過全域性鎖,但是這個效能會很差。為了優化這個效能,我們考慮可以每個執行緒分配一個執行緒本地私有的記憶體池,然後採用 bump-the-pointer 機制進行記憶體分配。這個執行緒本地私有的記憶體池,就是 TLAB。只有 TLAB 滿了,再去申請記憶體的時候,需要擴充 TLAB 或者使用新的 TLAB,這時候才需要鎖。這樣大大減少了鎖使用。
TLAB 初始化
TLAB 分配
GC 時 TLAB 回收與重計算期望大小
為何 Java 程式碼越執行越快 - TLAB預熱
根據之前的分析,每個執行緒的 TLAB 的大小,會根據執行緒分配的特性,不斷變化並趨於穩定,大小主要是由分配比例 EMA 決定,但是這個採集是需要一定執行次數的。並且 EMA 的前 100 次採集預設是不夠穩定的,所以 TLAB 大小也在程式一開始的時候變化頻繁。當程式執行緒趨於穩定,執行一段時間後, 每個執行緒 TLAB 大小也會趨於穩定並且調整到最適合這個執行緒物件分配特性的大小。這樣,就更接近最理想的只有 Eden 區滿了才會 GC,所有 Eden 區的物件都是通過 TLAB 分配的高效分配情況。這就是 Java 程式碼越執行越快在 TLAB 方面的原因。
每日一刷,輕鬆提升技術,斬獲各種offer: