今天,又是干貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。由於文章很長,每個人閱讀習慣不同,所以特此拆成單篇版和多篇版
- 全網最硬核 JVM TLAB 分析(單篇版不包含額外加菜)
- 全網最硬核 JVM TLAB 分析 1. 內存分配思想引入
- 全網最硬核 JVM TLAB 分析 2. TLAB生命周期與帶來的問題思考
- 全網最硬核 JVM TLAB 分析 3. JVM EMA期望算法與TLAB相關JVM啟動參數
- 全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析
- 全網最硬核 JVM TLAB 分析 5. TLAB 源代碼全解析
- 全網最硬核 JVM TLAB 分析 6. TLAB 相關熱門Q&A匯總
- 全網最硬核 JVM TLAB 分析(額外加菜) 7. TLAB 相關 JVM 日志解析
- 全網最硬核 JVM TLAB 分析(額外加菜) 8. 通過 JFR 監控 TLAB
1. 觀前提醒
本期內容比較硬核,非常全面,涉及到了設計思想到實現原理以及源碼,並且還給出了相應的日志以及監控方式,如果有不清楚或者有疑問的地方,歡迎留言。
其中涉及到的設計思想主要為個人理解,實現原理以及源碼解析也是個人整理,如果有不准確的地方,非常歡迎指正!提前感謝~~
2. 分配內存實現思路
我們經常會 new 一個對象,這個對象是需要占用空間的,第一次 new 一個對象占用的空間如 圖00 所示,
我們這里先只關心堆內部的存儲,元空間中的存儲,我們會在另一個系列詳細討論。堆內部的存儲包括對象頭,對象體以及內存對齊填充,那么這塊空間是如何分配的呢?
首先,對象所需的內存,在對象的類被解析加載進入元空間之后,就可以在分配內存創建前計算出來。假設現在我們自己來設計堆內存分配,一種最簡單的實現方式就是線性分配,也被稱為撞針分配(bump-the-pointer)。
每次需要分配內存時,先計算出需要的內存大小,然后 CAS 更新如 圖01 中所示的內存分配指針,標記分配的內存。但是內存一般不是這么整齊的,可能有些內存在分配有些內存就被釋放回收了。所以一般不會只靠撞針分配。一種思路是在撞針分配的基礎上,加上一個 FreeList。
簡單的實現是將釋放的對象內存加入 FreeList,下次分配對象的時候,優先從 FreeList 中尋找合適的內存大小進行分配,之后再在主內存中撞針分配。
這樣雖然一定程度上解決了問題,但是目前大多數應用是多線程的,所以內存分配是多線程的,都從主內存中分配,CAS 更新重試過於頻繁導致效率低下。目前的應用,一般根據不同業務區分了不同的線程池,在這種情況下,一般每個線程分配內存的特性是比較穩定的。這里的比較穩定指的是,每次分配對象的大小,每輪 GC 分配區間內的分配對象的個數以及總大小。所以,我們可以考慮每個線程分配內存后,就將這塊內存保留起來,用於下次分配,這樣就不用每次從主內存中分配了。如果能估算每輪 GC 內每個線程使用的內存大小,則可以提前分配好內存給線程,這樣就更能提高分配效率。這種內存分配的實現方式,在 JVM 中就是 TLAB (Thread Local Allocate Buffer)。
3. JVM 對象堆內存分配流程簡述
我們這里不考慮棧上分配,這些會在 JIT 的章節詳細分析,我們這里考慮的是無法棧上分配需要共享的對象。
對於 HotSpot JVM 實現,所有的 GC 算法的實現都是一種對於堆內存的管理,也就是都實現了一種堆的抽象,它們都實現了接口 CollectedHeap。當分配一個對象堆內存空間時,在 CollectedHeap 上首先都會檢查是否啟用了 TLAB,如果啟用了,則會嘗試 TLAB 分配;如果當前線程的 TLAB 大小足夠,那么從線程當前的 TLAB 中分配;如果不夠,但是當前 TLAB 剩余空間小於最大浪費空間限制(這是一個動態的值,我們后面會詳細分析),則從堆上(一般是 Eden 區) 重新申請一個新的 TLAB 進行分配。否則,直接在 TLAB 外進行分配。TLAB 外的分配策略,不同的 GC 算法不同。例如G1:
- 如果是 Humongous 對象(對象在超過 Region 一半大小的時候),直接在 Humongous 區域分配(老年代的連續區域)。
- 根據 Mutator 狀況在當前分配下標的 Region 內分配
4. TLAB 的生命周期
TLAB 是線程私有的,線程初始化的時候,會創建並初始化 TLAB。同時,在 GC 掃描對象發生之后,線程第一次嘗試分配對象的時候,也會創建並初始化 TLAB。
TLAB 生命周期停止(TLAB 聲明周期停止不代表內存被回收,只是代表這個 TLAB 不再被這個線程私有管理)在:
- 當前 TLAB 不夠分配,並且剩余空間小於最大浪費空間限制,那么這個 TLAB 會被退回 Eden,重新申請一個新的
- 發生 GC 的時候,TLAB 被回收。
5. TLAB 要解決的問題以及帶來的問題與解決方案的思考
TLAB 要解決的問題很明顯,盡量避免從堆上直接分配內存從而避免頻繁的鎖爭用。
引入 TLAB 之后,TLAB 的設計上,也有很多值得考慮的問題。
5.1. 引入 TLAB 后,會有內存孔隙問題,還可能影響 GC 掃描性能
出現孔隙的情況:
- 當前 TLAB 不夠分配時,如果剩余空間小於最大浪費空間限制,那么這個 TLAB 會被退回 Eden,重新申請一個新的。這個剩余空間就會成為孔隙。
- 當發生 GC 的時候,TLAB 沒有用完,沒有分配的內存也會成為孔隙。
如果不管這些孔隙,由於 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,如果不填充的話,外部並不知道哪一部分被使用哪一部分沒有,需要做額外的檢查,那么會影響 GC 掃描效率。所以 TLAB 回歸 Eden 的時候,會將剩余可用的空間用一個 dummy object 填充滿。如果填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記之后跳過這塊內存,增加掃描效率。但是同時,由於需要填充這個 dummy object,所以需要預留出這個對象的對象頭的空間。
5.2. 某個線程在一輪 GC 內分配的內存並不穩定
如果我們能提前知道在這一輪內每個線程會分配多少內存,那么我們可以直接提前分配好。但是,這簡直是痴人說夢。每個線程在每一輪 GC 的分配情況可能都是不一樣的:
- 不同的線程業務場景不同導致分配對象大小不同。我們一般會按照業務區分不同的線程池,做好線程池隔離。對於用戶請求,每次分配的對象可能比較小。對於后台分析請求,每次分配的對象相對大一些。
- 不同時間段內線程壓力並不均勻。業務是有高峰有低谷的,高峰時間段內肯定分配對象更多。
- 同一時間段同一線程池內的線程的業務壓力也不一定不能做到很均勻。很可能只有幾個線程很忙,其他線程很閑。
所以,綜合考慮以上情況,我們應該這么實現 TLAB:
- 不能一下子就給一個線程申請一個比較大的 TLAB,而是考慮這個線程 TLAB 分配滿之后再申請新的,這樣更加靈活。
- 每次申請 TLAB 的大小是變化的,並不是固定的。
- 每次申請 TLAB 的大小需要考慮當前 GC 輪次內會分配對象的線程的個數期望
- 每次申請 TLAB 的大小需要考慮所有線程期望 TLAB 分配滿重新申請新的 TLAB 次數
6. JVM 中的期望計算 EMA
在上面提到的 TLAB 大小設計的時候,我們經常提到期望。這個期望是根據歷史數據計算得出的,也就是每次輸入采樣值,根據歷史采樣值得出最新的期望值。不僅 TLAB 用到了這種期望計算,GC 和 JIT 等等 JVM 機制中都用到了。這里我們來看一種 TLAB 中經常用到的 EMA(Exponential Moving Average 指數平均數) 算法:
EMA 算法的核心在於設置合適的最小權重,我們假設一個場景:首先采樣100個 100(算法中的前 100 個是為了排除不穩定的干擾,我們這里直接忽略前 100 個采樣),之后采樣 50 個 2,最后采樣 50 個 200,對於不同的最小權重,來看一下變化曲線。
可以看出,最小權重越大,變化得越快,受歷史數據影響越小。根據應用設置合適的最小權重,可以讓你的期望更加理想。
這塊對應的源代碼:gcUtil.hpp
的 AdaptiveWeightedAverage
類。
7. TLAB 相關的 JVM 參數
這里僅僅是列出來,並附上簡介,看不懂沒關系,之后會有詳細分析,幫助你理解每一個參數。等你理解后,這個小章節就是你的工具書啦~~
以下參數以及默認值基於 OpenJDK 17
7.1. TLABStats(已過期)
從 Java 12 開始已過期,目前已經沒有相關的邏輯了。之前是用於 TLAB 統計數據從而更好地伸縮 TLAB 但是性能消耗相對較大,但是現在主要通過 EMA 計算了。
7.2. UseTLAB
說明:是否啟用 TLAB,默認是啟用的。
默認:true
舉例:如果想關閉:-XX:-UseTLAB
7.3. ZeroTLAB
說明:是否將新創建的 TLAB 內的所有字節歸零。我們創建一個類的時候,類的 field 是有默認值的,例如 boolean 是 false,int 是 0 等等,實現的方式就是對分配好的內存空間賦 0。設置 ZeroTLAB 為 true 代表在 TLAB 申請好的時候就賦 0,否則會在分配對象並初始化的時候賦 0.講道理,由於 TLAB 分配的時候會涉及到 Allocation Prefetch 優化 CPU 緩存,在 TLAB 分配好之后立刻更新賦 0 對於 CPU 緩存應該是更友好的,並且,如果 TLAB 沒有用滿,填充的 dummy object 其實依然是 0 數組,相當於大部分不用改。這么看來,開啟應該更好。但是ZeroTLAB 默認還是不開啟的。
默認:false
舉例:-XX:+ZeroTLAB
7.4. ResizeTLAB
說明:TLAB 是否是可變的,默認為是,也就是會根據線程歷史分配數據相關 EMA 計算出每次期望 TLAB 大小並以這個大小為准申請 TLAB。
默認:true
舉例:如果想關閉:-XX:-ResizeTLAB
7.5. TLABSize
說明:初始 TLAB 大小。單位是字節
默認:0, 0 就是不主動設置 TLAB 初始大小,而是通過 JVM 自己計算每一個線程的初始大小
舉例:-XX:TLABSize=65536
7.6. MinTLABSize
說明:最小 TLAB 大小。單位是字節
默認:2048
舉例:-XX:TLABSize=4096
7.7. TLABAllocationWeight
說明: TLAB 初始大小計算和線程數量有關,但是線程是動態創建銷毀的。所以需要基於歷史線程個數推測接下來的線程個數來計算 TLAB 大小。一般 JVM 內像這種預測函數都采用了 EMA 。這個參數就是 圖06 中的最小權重,權重越高,最近的數據占比影響越大。TLAB 重新計算大小是根據分配比例,分配比例也是采用了 EMA 算法,最小權重也是 TLABAllocationWeight
默認:35
舉例:-XX:TLABAllocationWeight=70
7.8. TLABWasteTargetPercent
說明:TLAB 的大小計算涉及到了 Eden 區的大小以及可以浪費的比率。TLAB 浪費指的是上面提到的重新申請新的 TLAB 的時候老的 TLAB 沒有分配的空間。這個參數其實就是 TLAB 浪費占用 Eden 的百分比,這個參數的作用會在接下來的原理說明內詳細說明
默認:1
舉例:-XX:TLABWasteTargetPercent=10
7.9. TLABRefillWasteFraction
說明: 初始最大浪費空間限制計算參數,初始最大浪費空間限制 = 當前期望 TLAB 大小 / TLABRefillWasteFraction
默認:64
舉例:-XX:TLABRefillWasteFraction=32
7.10. TLABWasteIncrement
說明: 最大浪費空間限制並不是不變的,在發生 TLAB 緩慢分配的時候(也就是當前 TLAB 空間不足以分配的時候),會增加最大浪費空間限制。這個參數就是 TLAB 緩慢分配時允許的 TLAB 浪費增量。單位不是字節,而是 MarkWord
個數,也就是 Java 堆的內存最小單元,64 位虛擬機的情況下,MarkWord
大小為 3 字節。
默認:4
舉例:-XX:TLABWasteIncrement=4
8.TLAB 基本流程
8.0. 如何設計每個線程的 TLAB 大小
之前我們提到了引入 TLAB 要面臨的問題以及解決方式,根據這些我們可以這么設計 TLAB。
首先,TLAB 的初始大小,應該和每個 GC 內需要對象分配的線程個數相關。但是,要分配的線程個數並不一定是穩定的,可能這個時間段線程數多,下個階段線程數就不那么多了,所以,需要用 EMA 的算法采集每個 GC 內需要對象分配的線程個數來計算這個個數期望。
接着,我們最理想的情況下,是每個 GC 內,所有用來分配對象的內存都處於對應線程的 TLAB 中。每個 GC 內用來分配對象的內存從 JVM 設計上來講,其實就是 Eden 區大小。在 最理想的情況下,最好只有Eden 區滿了的時候才會 GC,不會有其他原因導致的 GC,這樣是最高效的情況。Eden 區被用光,如果全都是 TLAB 內分配,也就是 Eden 區被所有線程的 TLAB 占滿了,這樣分配是最快的。
然后,每輪 GC 分配內存的線程個數以及大小是不一定的,如果一下子分配一大塊會造成浪費,如果太小則會頻繁從 Eden 申請 TLAB,降低效率。這個大小比較難以控制,但是我們可以限制每個線程究竟在一輪 GC 內,最多從 Eden 申請多少次 TLAB,這樣對於用戶來說更好控制。
最后,每個線程分配的內存大小,在每輪 GC 並不一定穩定,只用初始大小來指導之后的 TLAB 大小,顯然不夠。我們換個思路,每個線程分配的內存和歷史有一定關系因此我們可以從歷史分配中推測,所以每個線程也需要采用 EMA 的算法采集這個線程每次 GC 分配的內存,用於指導下次期望的 TLAB 的大小。
綜上所述,我們可以得出這樣一個近似的 TLAB 計算公式:
每個線程 TLAB 初始大小 = Eden區大小
/ (線程單個 GC 輪次內最多從 Eden 申請多少次 TLAB
* 當前 GC 分配線程個數 EMA
)
GC 后,重新計算 TLAB 大小 = Eden區大小
/ (線程單個 GC 輪次內最多從 Eden 申請多少次 TLAB
* 當前 GC 分配線程個數 EMA
)
接下來,我們來詳細分析 TLAB 的整個生命周期的每個流程。
8.1. TLAB 初始化
線程初始化的時候,如果 JVM 啟用了 TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB
關閉),則會初始化 TLAB,在發生對象分配時,會根據期望大小申請 TLAB 內存。同時,在 GC 掃描對象發生之后,線程第一次嘗試分配對象的時候,也會重新申請 TLAB 內存。我們先只關心初始化,初始化的流程圖如 圖08 所示:
初始化時候會計算 TLAB 初始期望大小。這涉及到了 TLAB 大小的限制:
- TLAB 的最小大小:通過
MinTLABSize
指定 - TLAB 的最大大小:不同的 GC 中不同,G1 GC 中為大對象(humongous object)大小,也就是 G1 region 大小的一半。因為開頭提到過,在 G1 GC 中,大對象不能在 TLAB 分配,而是老年代。ZGC 中為頁大小的 8 分之一,類似的在大部分情況下 Shenandoah GC 也是每個 Region 大小的 8 分之一。他們都是期望至少有 8 分之 7 的區域是不用退回的減少選擇 Cset 的時候的掃描復雜度。對於其他的 GC,則是 int 數組的最大大小,這個和之前提到的填充 dummy object 有關,后面會提到詳細流程。
之后的流程里面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的范圍內,為了避免啰嗦,我們不會再強調這個限制~~~!!! 之后的流程里面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的范圍內,為了避免啰嗦,我們不會再強調這個限制~~~!!! 之后的流程里面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的范圍內,為了避免啰嗦,我們不會再強調這個限制~~~!!! 重要的事情說三遍~
TLAB 期望大小(desired size) 在初始化的時候會計算 TLAB 期望大小,之后再 GC 等操作回收掉 TLAB 需要重計算這個期望大小。根據這個期望大小,TLAB 在申請空間的時候每次申請都會以這個期望大小作為基准的空間作為 TLAB 分配空間。
8.1.1. TLAB 初始期望大小計算
如 圖08 所示,如果指定了 TLABSize,就用這個大小作為初始期望大小。如果沒有指定,則按照如下的公式進行計算:
堆給TLAB的空間總大小
/(當前有效分配線程個數期望
*重填次數配置
)
- 堆給 TLAB 的空間總大小:堆上能有多少空間分配給 TLAB,不同的 GC 算法不一樣,但是大多數 GC 算法的實現都是 Eden 區大小,例如:
- 傳統的已經棄用的 Parallel Scanvage 中,就是 Eden 區大小。參考:parallelScavengeHeap.cpp
- 默認的G1 GC 中是 (YoungList 區域個數減去 Survivor 區域個數) * 區域大小,其實就是 Eden 區大小。參考:g1CollectedHeap.cpp
- ZGC 中是 Page 剩余空間大小,Page 類似於 Eden 區,是大部分對象分配的區域。參考:zHeap.cpp
- Shenandoah GC 中是 FreeSet 的大小,也是類似於 Eden 的概念。參考:shenandoahHeap.cpp
- 當前有效分配線程個數期望:這是一個全局 EMA,EMA 是什么之前已經說明了,是一種計算期望的方式。有效分配線程個數 EMA 的最小權重是 TLABAllocationWeight。有效分配線程個數 EMA 在有線程進行第一次有效對象分配的時候進行采集,在 TLAB 初始化的時候讀取這個值計算 TLAB 期望大小。
- TLAB 重填次數配置(refills time):根據 TLABWasteTargetPercent 計算的次數,公式為。TLABWasteTargetPercent 的意義其實是限制最大浪費空間限制,為何重填次數與之相關后面會詳細分析。
8.1.2. TLAB 初始分配比例計算
如 圖08 所示,接下來會計算TLAB 初始分配比例。
線程私有分配比例 EMA:與有效分配線程個數 EMA對應,有效分配線程個數 EMA是對於全局來說,每個線程應該占用多大的 TLAB 的描述,而分配比例 EMA 相當於對於當前線程應該占用的總 TLAB 空間的大小的一種動態控制。
初始化的時候,分配比例其實就是等於 1/當前有效分配線程個數
。圖08 的公式,代入之前的計算 TLAB 期望大小的公式,消參簡化之后就是1/當前有效分配線程個數
。這個值作為初始值,采集如線程私有的分配比例 EMA。
8.1.3. 清零線程私有統計數據
這些采集數據會用於之后的當前線程的分配比例的計算與采集,從而影響之后的當前線程 TLAB 期望大小。
8.2. TLAB 分配
TLAB 分配流程如 圖09 所示。
8.2.1. 從線程當前 TLAB 分配
如果啟用了 TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB
關閉),則首先從線程當前 TLAB 分配內存,如果分配成功則返回,否則根據當前 TLAB 剩余空間與當前最大浪費空間限制大小進行不同的分配策略。在下一個流程,就會提到這個限制究竟是什么。
8.2.2. 重新申請 TLAB 分配
如果當前 TLAB 剩余空間大於當前最大浪費空間限制(根據 圖08 的流程,我們知道這個初始值為 期望大小/TLABRefillWasteFraction),直接在堆上分配。否則,重新申請一個 TLAB 分配。
為什么需要最大浪費空間呢?
當重新分配一個 TLAB 的時候,原有的 TLAB 可能還有空間剩余。原有的 TLAB 被退回堆之前,需要填充好 dummy object。由於 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,如果不填充的話,外部並不知道哪一部分被使用哪一部分沒有,需要做額外的檢查,如果填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記之后跳過這塊內存,增加掃描效率。反正這塊內存已經屬於 TLAB,其他線程在下次掃描結束前是無法使用的。這個 dummy object 就是 int 數組。為了一定能有填充 dummy object 的空間,一般 TLAB 大小都會預留一個 dummy object 的 header 的空間,也是一個 int[]
的 header,所以 TLAB 的大小不能超過int 數組的最大大小,否則無法用 dummy object 填滿未使用的空間。
但是,填充 dummy 也造成了空間的浪費,這種浪費不能太多,所以通過最大浪費空間限制來限制這種浪費。
新的 TLAB 大小,取如下兩個值中較小的那個:
- 當前堆剩余給 TLAB 可分配的空間,大部分 GC 的實現其實就是對應的 Eden 區剩余大小:
- 傳統的已經棄用的 Parallel Scanvage 中,就是 Eden 區剩余大小。參考:parallelScavengeHeap.cpp
- 默認的G1 GC 中是當前 Region 中剩余大小,其實就是將 Eden 分區了。參考:g1CollectedHeap.cpp
- ZGC 中是 Page 剩余空間大小,Page 類似於 Eden 區,是大部分對象分配的區域。參考:zHeap.cpp
- Shenandoah GC 中是 FreeSet 的剩余大小,也是類似於 Eden 的概念。參考:shenandoahHeap.cpp
- TLAB 期望大小 + 當前需要分配的空間大小
當分配出來 TLAB 之后,根據 ZeroTLAB 配置,決定是否將每個字節賦 0。在創建對象的時候,本來也要對每個字段賦初始值,大部分字段初始值都是 0,並且,在 TLAB 返還到堆時,剩余空間填充的也是 int[] 數組,里面都是 0。所以其實可以提前填充好。並且,TLAB 剛分配出來的時候,賦 0 也能利用好 Allocation prefetch 的機制適應 CPU 緩存行(Allocation prefetch 的機制會在另一個系列說明),所以可以通過打開 ZeroTLAB 來在分配 TLAB 空間之后立刻賦 0。
8.2.3. 直接從堆上分配
直接從堆上分配是最慢的分配方式。一種情況就是,如果當前 TLAB 剩余空間大於當前最大浪費空間限制,直接在堆上分配。並且,還會增加當前最大浪費空間限制,每次有這樣的分配就會增加 TLABWasteIncrement 的大小,這樣在一定次數的直接堆上分配之后,當前最大浪費空間限制一直增大會導致當前 TLAB 剩余空間小於當前最大浪費空間限制,從而申請新的 TLAB 進行分配。
8.3. GC 時 TLAB 回收與重計算期望大小
相關流程如 圖10 所示,在 GC 前與 GC 后,都會對 TLAB 做一些操作。
8.3.1. GC 前的操作
在 GC 前,如果啟用了 TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB
關閉),則需要將所有線程的 TLAB 填充 dummy Object 退還給堆,並計算並采樣一些東西用於以后的 TLAB 大小計算。
首先為了保證本次計算具有參考意義,需要先判斷是否堆上 TLAB 空間被用了一半以上,假設不足,那么認為本輪 GC 的數據沒有參考意義。如果被用了一半以上,那么計算新的分配比例,新的分配比例 = 線程本輪 GC 分配空間的大小 / 堆上所有線程 TLAB 使用的空間,這么計算主要因為分配比例描述的是當前線程占用堆上所有給 TLAB 的空間的比例,每個線程不一樣,通過這個比例動態控制不同業務線程的 TLAB 大小。
線程本輪 GC 分配空間的大小包含 TLAB 中分配的和 TLAB 外分配的,從 圖8、圖9、圖10 流程圖中對於線程記錄中的線程分配空間大小的記錄就能看出,讀取出線程分配空間大小減去上一輪 GC 結束時線程分配空間大小就是線程本輪 GC 分配空間的大小。
最后,將當前 TLAB 填充好 dummy object 之后,返還給堆。
8.3.2. GC 后的操作
如果啟用了 TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB
關閉),以及 TLAB 大小可變(默認是啟用的, 可以通過 -XX:-ResizeTLAB
關閉),那么在 GC 后會重新計算每個線程 TLAB 的期望大小,新的期望大小 = 堆給TLAB的空間總大小 * 當前分配比例 EMA / 重填次數配置。然后會重置最大浪費空間限制,為當前 期望大小 / TLABRefillWasteFraction。
9. OpenJDK HotSpot TLAB 相關源代碼分析
如果這里看的比較吃力,可以直接看第 10 章,熱門 Q&A,里面有很多大家常問的問題
9.1. TLAB 類構成
線程初始化的時候,如果 JVM 啟用了 TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB 關閉),則會初始化 TLAB。
TLAB 包括如下幾個 field (HeapWord* 可以理解為堆中的內存地址):
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
//靜態全局變量
static size_t _max_size; // 所有 TLAB 的最大大小
static int _reserve_for_allocation_prefetch; // CPU 緩存優化 Allocation Prefetch 的保留空間,這里先不用關心
static unsigned _target_refills; //每個 GC 周期內期望的重填次數
//以下是 TLAB 的主要構成 field
HeapWord* _start; // TLAB 起始地址,表示堆內存地址都用 HeapWord*
HeapWord* _top; // 上次分配的內存地址
HeapWord* _end; // TLAB 結束地址
size_t _desired_size; // TLAB 大小 包括保留空間,表示內存大小都需要通過 size_t 類型,也就是實際字節數除以 HeapWordSize 的值
size_t _refill_waste_limit; // TLAB最大浪費空間,剩余空間不足分配浪費空間限制。在TLAB剩余空間不足的時候,根據這個值決定分配策略,如果浪費空間大於這個值則直接在 Eden 區分配,如果小於這個值則將當前 TLAB 放回 Eden 區管理並從 Eden 申請新的 TLAB 進行分配。
AdaptiveWeightedAverage _allocation_fraction; // 當前 TLAB 分配比例 EMA
//以下是我們這里不用太關心的 field
HeapWord* _allocation_end; // TLAB 真正可以用來分配內存的結束地址,這個是 _end 結束地址排除保留空間(預留給 dummy object 的對象頭空間)
HeapWord* _pf_top; // Allocation Prefetch CPU 緩存優化機制相關需要的參數,這里先不用考慮
size_t _allocated_before_last_gc; // 這個用於計算 圖10 中的線程本輪 GC 分配空間的大小,記錄上次 GC 時,線程分配的空間大小
unsigned _number_of_refills; // 線程分配內存數據采集相關,TLAB 剩余空間不足分配次數
unsigned _fast_refill_waste; // 線程分配內存數據采集相關,TLAB 快速分配浪費,快速分配就是直接在 TLAB 分配,這個在現在 JVM 中已經用不到了
unsigned _slow_refill_waste; // 線程分配內存數據采集相關,TLAB 慢速分配浪費,慢速分配就是重填一個 TLAB 分配
unsigned _gc_waste; // 線程分配內存數據采集相關,gc浪費
unsigned _slow_allocations; // 線程分配內存數據采集相關,TLAB 慢速分配計數
size_t _allocated_size; // 分配的內存大小
size_t _bytes_since_last_sample_point; // JVM TI 采集指標相關 field,這里不用關心
9.2. TLAB 初始化
首先是 JVM 啟動的時候,全局 TLAB 需要初始化:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::startup_initialization() {
//初始化,也就是歸零統計數據
ThreadLocalAllocStats::initialize();
// 假設平均下來,GC 掃描的時候,每個線程當前的 TLAB 都有一半的內存被浪費,這個每個線程使用內存的浪費的百分比率(也就是 TLABWasteTargetPercent),也就是等於(注意,僅最新的那個 TLAB 有浪費,之前 refill 退回的假設是沒有浪費的):1/2 * (每個 epoch 內每個線程期望 refill 次數) * 100
//那么每個 epoch 內每個線程 refill 次數配置就等於 50 / TLABWasteTargetPercent, 默認也就是 50 次。
_target_refills = 100 / (2 * TLABWasteTargetPercent);
// 但是初始的 _target_refills 需要設置最多不超過 2 次來減少 VM 初始化時候 GC 的可能性
_target_refills = MAX2(_target_refills, 2U);
//如果 C2 JIT 編譯存在並啟用,則保留 CPU 緩存優化 Allocation Prefetch 空間,這個這里先不用關心,會在別的章節講述
#ifdef COMPILER2
if (is_server_compilation_mode_vm()) {
int lines = MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;
_reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /
(int)HeapWordSize;
}
#endif
// 初始化 main 線程的 TLAB
guarantee(Thread::current()->is_Java_thread(), "tlab initialization thread not Java thread");
Thread::current()->tlab().initialize();
log_develop_trace(gc, tlab)("TLAB min: " SIZE_FORMAT " initial: " SIZE_FORMAT " max: " SIZE_FORMAT,
min_size(), Thread::current()->tlab().initial_desired_size(), max_size());
}
每個線程維護自己的 TLAB,同時每個線程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,線程數量,還有線程的對象分配速率決定。
在 Java 線程開始運行時,會先分配 TLAB:
src/hotspot/share/runtime/thread.cpp
void JavaThread::run() {
// initialize thread-local alloc buffer related fields
this->initialize_tlab();
//剩余代碼忽略
}
分配 TLAB 其實就是調用 ThreadLocalAllocBuffer 的 initialize 方法。
src/hotspot/share/runtime/thread.hpp
void initialize_tlab() {
//如果沒有通過 -XX:-UseTLAB 禁用 TLAB,則初始化TLAB
if (UseTLAB) {
tlab().initialize();
}
}
// Thread-Local Allocation Buffer (TLAB) support
ThreadLocalAllocBuffer& tlab() {
return _tlab;
}
ThreadLocalAllocBuffer _tlab;
ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我們要關心的各種 field:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::initialize() {
//設置初始指針,由於還沒有從 Eden 分配內存,所以這里都設置為 NULL
initialize(NULL, // start
NULL, // top
NULL); // end
//計算初始期望大小,並設置
set_desired_size(initial_desired_size());
//所有 TLAB 總大小,不同的 GC 實現有不同的 TLAB 容量, 一般是 Eden 區大小
//例如 G1 GC,就是等於 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解為年輕代減去Survivor區,也就是Eden區
size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;
//計算這個線程的 TLAB 期望占用所有 TLAB 總體大小比例
//TLAB 期望占用大小也就是這個 TLAB 大小乘以期望 refill 的次數
float alloc_frac = desired_size() * target_refills() / (float) capacity;
//記錄下來,用於計算 EMA
_allocation_fraction.sample(alloc_frac);
//計算初始 refill 最大浪費空間,並設置
//如前面原理部分所述,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction
set_refill_waste_limit(initial_refill_waste_limit());
//重置統計
reset_statistics();
}
9.2.1. 初始期望大小是如何計算的呢?
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
//計算初始大小
size_t ThreadLocalAllocBuffer::initial_desired_size() {
size_t init_sz = 0;
//如果通過 -XX:TLABSize 設置了 TLAB 大小,則用這個值作為初始期望大小
//表示堆內存占用大小都需要用占用幾個 HeapWord 表示,所以用TLABSize / HeapWordSize
if (TLABSize > 0) {
init_sz = TLABSize / HeapWordSize;
} else {
//獲取當前epoch內線程數量期望,這個如之前所述通過 EMA 預測
unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();
//不同的 GC 實現有不同的 TLAB 容量,Universe::heap()->tlab_capacity(thread()) 一般是 Eden 區大小
//例如 G1 GC,就是等於 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解為年輕代減去Survivor區,也就是Eden區
//整體大小等於 Eden區大小/(當前 epcoh 內會分配對象期望線程個數 * 每個 epoch 內每個線程 refill 次數配置)
//target_refills已經在 JVM 初始化所有 TLAB 全局配置的時候初始化好了
init_sz = (Universe::heap()->tlab_capacity(thread()) / HeapWordSize) /
(nof_threads * target_refills());
//考慮對象對齊,得出最后的大小
init_sz = align_object_size(init_sz);
}
//保持大小在 min_size() 還有 max_size() 之間
//min_size主要由 MinTLABSize 決定
init_sz = MIN2(MAX2(init_sz, min_size()), max_size());
return init_sz;
}
//最小大小由 MinTLABSize 決定,需要表示為 HeapWordSize,並且考慮對象對齊,最后的 alignment_reserve 是 dummy object 填充的對象頭大小(這里先不考慮 JVM 的 CPU 緩存 prematch,我們會在其他章節詳細分析)。
static size_t min_size() {
return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve();
}
9.2.2. TLAB 最大大小是怎樣決定的呢?
不同的 GC 方式,有不同的方式:
G1 GC 中為大對象(humongous object)大小,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp
// For G1 TLABs should not contain humongous objects, so the maximum TLAB size
// must be equal to the humongous object limit.
size_t G1CollectedHeap::max_tlab_size() const {
return align_down(_humongous_object_threshold_in_words, MinObjAlignment);
}
ZGC 中為頁大小的 8 分之一,類似的在大部分情況下 Shenandoah GC 也是每個 Region 大小的 8 分之一。他們都是期望至少有 8 分之 7 的區域是不用退回的減少選擇 Cset 的時候的掃描復雜度:
src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp
MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);
src/hotspot/share/gc/z/zHeap.cpp
const size_t ZObjectSizeLimitSmall = ZPageSizeSmall / 8;
對於其他的 GC,則是 int 數組的最大大小,這個和為了填充 dummy object 表示 TLAB 的空區域有關。這個原因之前已經說明了。
9.3. TLAB 分配內存
當 new 一個對象時,需要調用instanceOop InstanceKlass::allocate_instance(TRAPS)
src/hotspot/share/oops/instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.
instanceOop i;
i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
其核心就是heap()->obj_allocate(this, size, CHECK_NULL)
從堆上面分配內存:
src/hotspot/share/gc/shared/collectedHeap.inline.hpp
inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
ObjAllocator allocator(klass, size, THREAD);
return allocator.allocate();
}
使用全局的 ObjAllocator
實現進行對象內存分配:
src/hotspot/share/gc/shared/memAllocator.cpp
oop MemAllocator::allocate() const {
oop obj = NULL;
{
Allocation allocation(*this, &obj);
//分配堆內存,繼續看下面一個方法
HeapWord* mem = mem_allocate(allocation);
if (mem != NULL) {
obj = initialize(mem);
} else {
// The unhandled oop detector will poison local variable obj,
// so reset it to NULL if mem is NULL.
obj = NULL;
}
}
return obj;
}
HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
//如果使用了 TLAB,則從 TLAB 分配,分配代碼繼續看下面一個方法
if (UseTLAB) {
HeapWord* result = allocate_inside_tlab(allocation);
if (result != NULL) {
return result;
}
}
//否則直接從 tlab 外分配
return allocate_outside_tlab(allocation);
}
HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
assert(UseTLAB, "should use UseTLAB");
//從當前線程的 TLAB 分配內存,TLAB 快分配
HeapWord* mem = _thread->tlab().allocate(_word_size);
//如果沒有分配失敗則返回
if (mem != NULL) {
return mem;
}
//如果分配失敗則走 TLAB 慢分配,需要 refill 或者直接從 Eden 分配
return allocate_inside_tlab_slow(allocation);
}
9.3.1. TLAB 快分配
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
//驗證各個內存指針有效,也就是 _top 在 _start 和 _end 范圍內
invariants();
HeapWord* obj = top();
//如果空間足夠,則分配內存
if (pointer_delta(end(), obj) >= size) {
set_top(obj + size);
invariants();
return obj;
}
return NULL;
}
9.3.2. TLAB 慢分配
src/hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const {
HeapWord* mem = NULL;
ThreadLocalAllocBuffer& tlab = _thread->tlab();
// 如果 TLAB 剩余空間大於 最大浪費空間,則記錄並讓最大浪費空間遞增
if (tlab.free() > tlab.refill_waste_limit()) {
tlab.record_slow_allocation(_word_size);
return NULL;
}
//重新計算 TLAB 大小
size_t new_tlab_size = tlab.compute_size(_word_size);
//TLAB 放回 Eden 區
tlab.retire_before_allocation();
if (new_tlab_size == 0) {
return NULL;
}
// 計算最小大小
size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size);
//分配新的 TLAB 空間,並在里面分配對象
mem = Universe::heap()->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size);
if (mem == NULL) {
assert(allocation._allocated_tlab_size == 0,
"Allocation failed, but actual size was updated. min: " SIZE_FORMAT
", desired: " SIZE_FORMAT ", actual: " SIZE_FORMAT,
min_tlab_size, new_tlab_size, allocation._allocated_tlab_size);
return NULL;
}
assert(allocation._allocated_tlab_size != 0, "Allocation succeeded but actual size not updated. mem at: "
PTR_FORMAT " min: " SIZE_FORMAT ", desired: " SIZE_FORMAT,
p2i(mem), min_tlab_size, new_tlab_size);
//如果啟用了 ZeroTLAB 這個 JVM 參數,則將對象所有字段置零值
if (ZeroTLAB) {
// ..and clear it.
Copy::zero_to_words(mem, allocation._allocated_tlab_size);
} else {
// ...and zap just allocated object.
}
//設置新的 TLAB 空間為當前線程的 TLAB
tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);
//返回分配的對象內存地址
return mem;
}
9.3.2.1 TLAB最大浪費空間
TLAB最大浪費空間 _refill_waste_limit
初始值為 TLAB 大小除以 TLABRefillWasteFraction:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp
size_t initial_refill_waste_limit() { return desired_size() / TLABRefillWasteFraction; }
每次慢分配,調用record_slow_allocation(size_t obj_size)
記錄慢分配的同時,增加 TLAB 最大浪費空間的大小:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) {
//每次慢分配,_refill_waste_limit 增加 refill_waste_limit_increment,也就是 TLABWasteIncrement
set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment());
_slow_allocations++;
log_develop_trace(gc, tlab)("TLAB: %s thread: " INTPTR_FORMAT " [id: %2d]"
" obj: " SIZE_FORMAT
" free: " SIZE_FORMAT
" waste: " SIZE_FORMAT,
"slow", p2i(thread()), thread()->osthread()->thread_id(),
obj_size, free(), refill_waste_limit());
}
//refill_waste_limit_increment 就是 JVM 參數 TLABWasteIncrement
static size_t refill_waste_limit_increment() { return TLABWasteIncrement; }
9.3.2.2. 重新計算 TLAB 大小
重新計算會取 當前堆剩余給 TLAB 可分配的空間 和 TLAB 期望大小 + 當前需要分配的空間大小 中的小的那個:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) {
//獲取當前堆剩余給 TLAB 可分配的空間
const size_t available_size = Universe::heap()->unsafe_max_tlab_alloc(thread()) / HeapWordSize;
//取 TLAB 可分配的空間 和 TLAB 期望大小 + 當前需要分配的空間大小 以及 TLAB 最大大小中的小的那個
size_t new_tlab_size = MIN3(available_size, desired_size() + align_object_size(obj_size), max_size());
// 確保大小大於 dummy obj 對象頭
if (new_tlab_size < compute_min_size(obj_size)) {
log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns failure",
obj_size);
return 0;
}
log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns " SIZE_FORMAT,
obj_size, new_tlab_size);
return new_tlab_size;
}
9.3.2.3. 當前 TLAB 放回堆
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
//在TLAB慢分配被調用,當前 TLAB 放回堆
void ThreadLocalAllocBuffer::retire_before_allocation() {
//將當前 TLAB 剩余空間大小加入慢分配浪費空間大小
_slow_refill_waste += (unsigned int)remaining();
//執行 TLAB 退還給堆,這個在后面 GC 的時候還會被調用用於將所有的線程的 TLAB 退回堆
retire();
}
//對於 TLAB 慢分配,stats 為空
//對於 GC 的時候調用,stats 用於記錄每個線程的數據
void ThreadLocalAllocBuffer::retire(ThreadLocalAllocStats* stats) {
if (stats != NULL) {
accumulate_and_reset_statistics(stats);
}
//如果當前 TLAB 有效
if (end() != NULL) {
invariants();
//將用了的空間記錄如線程分配對象大小記錄
thread()->incr_allocated_bytes(used_bytes());
//填充dummy object
insert_filler();
//清空當前 TLAB 指針
initialize(NULL, NULL, NULL);
}
}
9.4. GC 相關 TLAB 操作
9.4.1. GC 前
不同的 GC 可能實現不一樣,但是 TLAB 操作的時機是基本一樣的,這里以 G1 GC 為例,在真正 GC 前:
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
void G1CollectedHeap::gc_prologue(bool full) {
//省略其他代碼
// Fill TLAB's and such
{
Ticks start = Ticks::now();
//確保堆內存是可以解析的
ensure_parsability(true);
Tickspan dt = Ticks::now() - start;
phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS);
}
//省略其他代碼
}
為何要確保堆內存是可以解析的呢?這樣有利於更快速的掃描堆上對象。確保內存可以解析里面做了什么呢?其實主要就是退還每個線程的 TLAB 以及填充 dummy object。
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
void CollectedHeap::ensure_parsability(bool retire_tlabs) {
//真正的 GC 肯定發生在安全點上,這個在后面安全點章節會詳細說明
assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
"Should only be called at a safepoint or at start-up");
ThreadLocalAllocStats stats;
for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) {
BarrierSet::barrier_set()->make_parsable(thread);
//如果全局啟用了 TLAB
if (UseTLAB) {
//如果指定要回收,則回收 TLAB
if (retire_tlabs) {
//回收 TLAB,調用 9.3.2.3. 當前 TLAB 放回堆 提到的 retire 方法
thread->tlab().retire(&stats);
} else {
//當前如果不回收,則將 TLAB 填充 Dummy Object 利於解析
thread->tlab().make_parsable();
}
}
}
stats.publish();
}
9.4.2. GC 后
不同的 GC 可能實現不一樣,但是 TLAB 操作的時機是基本一樣的,這里以 G1 GC 為例,在 GC 后:
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
_desired_size
是什么時候變得呢?怎么變得呢?
void G1CollectedHeap::gc_epilogue(bool full) {
//省略其他代碼
resize_all_tlabs();
}
src/hotspot/share/gc/shared/collectedHeap.cpp
void CollectedHeap::resize_all_tlabs() {
//需要在安全點,GC 會處於安全點的
assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
"Should only resize tlabs at safepoint");
//如果 UseTLAB 和 ResizeTLAB 都是打開的(默認就是打開的)
if (UseTLAB && ResizeTLAB) {
for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) {
//重新計算每個線程 TLAB 期望大小
thread->tlab().resize();
}
}
}
重新計算每個線程 TLAB 期望大小:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::resize() {
assert(ResizeTLAB, "Should not call this otherwise");
//根據 _allocation_fraction 這個 EMA 采集得出平均數乘以Eden區大小,得出 TLAB 當前預測占用內存比例
size_t alloc = (size_t)(_allocation_fraction.average() *
(Universe::heap()->tlab_capacity(thread()) / HeapWordSize));
//除以目標 refill 次數就是新的 TLAB 大小,和初始化時候的計算方法差不多
size_t new_size = alloc / _target_refills;
//保證在 min_size 還有 max_size 之間
new_size = clamp(new_size, min_size(), max_size());
size_t aligned_new_size = align_object_size(new_size);
log_trace(gc, tlab)("TLAB new size: thread: " INTPTR_FORMAT " [id: %2d]"
" refills %d alloc: %8.6f desired_size: " SIZE_FORMAT " -> " SIZE_FORMAT,
p2i(thread()), thread()->osthread()->thread_id(),
_target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size);
//設置新的 TLAB 大小
set_desired_size(aligned_new_size);
//重置 TLAB 最大浪費空間
set_refill_waste_limit(initial_refill_waste_limit());
}
10. TLAB 流程常見問題 Q&A
這里我會持續更新的,解決大家的各種疑問
10.1. 為何 TLAB 在退還給堆的時候需要填充 dummy object
主要保證 GC 的時候掃描高效。由於 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,如果不填充的話,外部並不知道哪一部分被使用哪一部分沒有,需要做額外的檢查,如果填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記之后跳過這塊內存,增加掃描效率。反正這塊內存已經屬於 TLAB,其他線程在下次掃描結束前是無法使用的。這個 dummy object 就是 int 數組。為了一定能有填充 dummy object 的空間,一般 TLAB 大小都會預留一個 dummy object 的 header 的空間,也是一個 int[]
的 header,所以 TLAB 的大小不能超過int 數組的最大大小,否則無法用 dummy object 填滿未使用的空間。
10.2. 為何 TLAB 需要最大浪費空間限制
當重新分配一個 TLAB 的時候,原有的 TLAB 可能還有空間剩余。原有的 TLAB 被退回堆之前,需要填充好 dummy object。這樣導致這塊內存無法分配對象,所示被稱為“浪費”。如果不限制,遇到 TLAB 剩余空間不足的情況就會重新申請,導致分配效率降低,大部分空間被 dummy object 占滿了,導致 GC 更加頻繁。
10.3. 為何 TLAB 重填次數配置 等於 100 / (2 * TLABWasteTargetPercent)
TLABWasteTargetPercent 描述了初始最大浪費空間配置占 TLAB 的比例
首先,最理想的情況就是盡量讓所有對象在 TLAB 內分配,也就是 TLAB 可能要占滿 Eden。
在下次 GC 掃描前,退回 Eden 的內存別的線程是不能用的,因為剩余空間已經填滿了 dummy object。所以所有線程使用內存大小就是 下個 epcoh 內會分配對象期望線程個數 * 每個 epoch 內每個線程 refill 次數配置
,對象一般都在 Eden 區由某個線程分配,也就所有線程使用內存大小就最好是整個 Eden。但是這種情況太過於理想,總會有內存被填充了 dummy object而造成了浪費,因為 GC 掃描隨時可能發生。假設平均下來,GC 掃描的時候,每個線程當前的 TLAB 都有一半的內存被浪費,這個每個線程使用內存的浪費的百分比率(也就是 TLABWasteTargetPercent),也就是等於(注意,僅最新的那個 TLAB 有浪費,之前 refill 退回的假設是沒有浪費的):
1/2 * (每個 epoch 內每個線程期望 refill 次數) * 100
那么每個 epoch 內每個線程 refill 次數配置就等於 50 / TLABWasteTargetPercent
, 默認也就是 50 次。
10.4. 為何考慮 ZeroTLAB
當分配出來 TLAB 之后,根據 ZeroTLAB 配置,決定是否將每個字節賦 0。在 TLAB 申請時,由於申請 TLAB 都發生在對象分配的時候,也就是這塊內存會立刻被使用,並修改賦值。操作內存,涉及到 CPU 緩存行,如果是多核環境,還會涉及到 CPU 緩存行 false sharing,為了優化,JVM 在這里做了 Allocation Prefetch,簡單理解就是分配 TLAB 的時候,會盡量加載這塊內存到 CPU 緩存,也就是在分配 TLAB 內存的時候,修改內存是最高效的。
在創建對象的時候,本來也要對每個字段賦初始值,大部分字段初始值都是 0,並且,在 TLAB 返還到堆時,剩余空間填充的也是 int[] 數組,里面都是 0。
所以,TLAB 剛分配出來的時候,賦 0 避免了后續再賦 0。也能利用好 Allocation prefetch 的機制適應 CPU 緩存行(Allocation prefetch 的機制詳情會在另一個系列說明)
10.5. 為何 JVM 需要預熱,為什么 Java 代碼越執行越快(這里只提 TLAB 相關的,JIT,MetaSpace,GC等等其他系列會說)
根據之前的分析,每個線程的 TLAB 的大小,會根據線程分配的特性,不斷變化並趨於穩定,大小主要是由分配比例 EMA 決定,但是這個采集是需要一定運行次數的。並且 EMA 的前 100 次采集默認是不夠穩定的,所以 TLAB 大小也在程序一開始的時候變化頻繁。當程序線程趨於穩定,運行一段時間后, 每個線程 TLAB 大小也會趨於穩定並且調整到最適合這個線程對象分配特性的大小。這樣,就更接近最理想的只有 Eden 區滿了才會 GC,所有 Eden 區的對象都是通過 TLAB 分配的高效分配情況。這就是 Java 代碼越執行越快在 TLAB 方面的原因。