90% 的 Java 程序員都說不上來的為何 Java 代碼越執行越快(2)- TLAB預熱


經常聽到 Java 性能不如 C/C++ 的言論,也經常聽說 Java 程序需要預熱,那么其中主要原因是啥呢

面試的時候談到 JVM,也有很多面試官喜歡問,為啥 Java 程序越執行越快呢

一般人都能回答上來,類加載,緩存預熱等等,但是深入下去,最重要的卻沒有答上來,今天本系列文章就來幫助大家理解這個問題的關鍵。本篇文章是 TLAB 預熱。

TLAB(Thread Local Allocation Buffer)線程本地分配緩存區,這是一個線程專用的內存分配區域。

image

既然是一個內存分配區域,我們就先要搞清楚 Java 內存大概是如何分配的。

image

我們這里不考慮棧上分配,這些會在 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 初始化

image

TLAB 分配

image

GC 時 TLAB 回收與重計算期望大小

image

為何 Java 代碼越執行越快 - TLAB預熱

根據之前的分析,每個線程的 TLAB 的大小,會根據線程分配的特性,不斷變化並趨於穩定,大小主要是由分配比例 EMA 決定,但是這個采集是需要一定運行次數的。並且 EMA 的前 100 次采集默認是不夠穩定的,所以 TLAB 大小也在程序一開始的時候變化頻繁。當程序線程趨於穩定,運行一段時間后, 每個線程 TLAB 大小也會趨於穩定並且調整到最適合這個線程對象分配特性的大小。這樣,就更接近最理想的只有 Eden 區滿了才會 GC,所有 Eden 區的對象都是通過 TLAB 分配的高效分配情況。這就是 Java 代碼越執行越快在 TLAB 方面的原因。

每日一刷,輕松提升技術,斬獲各種offer:

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM