全網最硬核 JVM TLAB 分析(單篇版不包含額外加菜)

今天,又是乾貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。因爲文章很長,每一個人閱讀習慣不一樣,因此特此拆成單篇版和多篇版git

1. 觀前提醒

本期內容比較硬核,很是全面,涉及到了設計思想到實現原理以及源碼,而且還給出了相應的日誌以及監控方式,若是有不清楚或者有疑問的地方,歡迎留言。github

其中涉及到的設計思想主要爲我的理解,實現原理以及源碼解析也是我的整理,若是有不許確的地方,很是歡迎指正!提早感謝~~算法

2. 分配內存實現思路

咱們常常會 new 一個對象,這個對象是須要佔用空間的,第一次 new 一個對象佔用的空間如 圖00 所示,數組

`MetaSpace`

咱們這裏先只關心堆內部的存儲,元空間中的存儲,咱們會在另外一個系列詳細討論。堆內部的存儲包括對象頭,對象體以及內存對齊填充,那麼這塊空間是如何分配的呢?緩存

首先,對象所需的內存,在對象的類被解析加載進入元空間以後,就能夠在分配內存建立前計算出來。假設如今咱們本身來設計堆內存分配,一種最簡單的實現方式就是線性分配,也被稱爲撞針分配(bump-the-pointer)。安全

image

每次須要分配內存時,先計算出須要的內存大小,而後 CAS 更新圖01 中所示的內存分配指針,標記分配的內存。可是內存通常不是這麼整齊的,可能有些內存在分配有些內存就被釋放回收了。因此通常不會只靠撞針分配。一種思路是在撞針分配的基礎上,加上一個 FreeList。多線程

image

簡單的實現是將釋放的對象內存加入 FreeList,下次分配對象的時候,優先從 FreeList 中尋找合適的內存大小進行分配,以後再在主內存中撞針分配。ide

這樣雖然必定程度上解決了問題,可是目前大多數應用是多線程的,因此內存分配是多線程的,都從主內存中分配,CAS 更新重試過於頻繁致使效率低下。目前的應用,通常根據不一樣業務區分了不一樣的線程池,在這種狀況下,通常每一個線程分配內存的特性是比較穩定的。這裏的比較穩定指的是,每次分配對象的大小,每輪 GC 分配區間內的分配對象的個數以及總大小。因此,咱們能夠考慮每一個線程分配內存後,就將這塊內存保留起來,用於下次分配,這樣就不用每次從主內存中分配了。若是能估算每輪 GC 內每一個線程使用的內存大小,則能夠提早分配好內存給線程,這樣就更能提升分配效率。這種內存分配的實現方式,在 JVM 中就是 TLAB (Thread Local Allocate Buffer)。函數

3. JVM 對象堆內存分配流程簡述

image

咱們這裏不考慮棧上分配,這些會在 JIT 的章節詳細分析,咱們這裏考慮的是沒法棧上分配須要共享的對象工具

對於 HotSpot JVM 實現,全部的 GC 算法的實現都是一種對於堆內存的管理,也就是都實現了一種堆的抽象,它們都實現了接口 CollectedHeap。當分配一個對象堆內存空間時,在 CollectedHeap 上首先都會檢查是否啓用了 TLAB,若是啓用了,則會嘗試 TLAB 分配;若是當前線程的 TLAB 大小足夠,那麼從線程當前的 TLAB 中分配;若是不夠,可是當前 TLAB 剩餘空間小於最大浪費空間限制(這是一個動態的值,咱們後面會詳細分析),則從堆上(通常是 Eden 區) 從新申請一個新的 TLAB 進行分配。不然,直接在 TLAB 外進行分配。TLAB 外的分配策略,不一樣的 GC 算法不一樣。例如G1:

  • 若是是 Humongous 對象(對象在超過 Region 一半大小的時候),直接在 Humongous 區域分配(老年代的連續區域)。
  • 根據 Mutator 情況在當前分配下標的 Region 內分配

4. TLAB 的生命週期

image

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 沒有用完,沒有分配的內存也會成爲孔隙。

image

若是無論這些孔隙,因爲 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 指數平均數) 算法:

image

EMA 算法的核心在於設置合適的最小權重,咱們假設一個場景:首先採樣100個 100(算法中的前 100 個是爲了排除不穩定的干擾,咱們這裏直接忽略前 100 個採樣),以後採樣 50 個 2,最後採樣 50 個 200,對於不一樣的最小權重,來看一下變化曲線。

image

能夠看出,最小權重越大,變化得越快,受歷史數據影響越小。根據應用設置合適的最小權重,可讓你的指望更加理想。

這塊對應的源代碼:gcUtil.hppAdaptiveWeightedAverage 類。

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 所示:

image

初始化時候會計算 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的空間總大小/(當前有效分配線程個數指望*重填次數配置)

  1. 堆給 TLAB 的空間總大小:堆上能有多少空間分配給 TLAB,不一樣的 GC 算法不同,可是大多數 GC 算法的實現都是 Eden 區大小,例如:
    1. 傳統的已經棄用的 Parallel Scanvage 中,就是 Eden 區大小。參考:parallelScavengeHeap.cpp
    2. 默認的G1 GC 中是 (YoungList 區域個數減去 Survivor 區域個數) * 區域大小,其實就是 Eden 區大小。參考:g1CollectedHeap.cpp
    3. ZGC 中是 Page 剩餘空間大小,Page 相似於 Eden 區,是大部分對象分配的區域。參考:zHeap.cpp
    4. Shenandoah GC 中是 FreeSet 的大小,也是相似於 Eden 的概念。參考:shenandoahHeap.cpp
  2. 當前有效分配線程個數指望:這是一個全局 EMA,EMA 是什麼以前已經說明了,是一種計算指望的方式。有效分配線程個數 EMA 的最小權重是 TLABAllocationWeight。有效分配線程個數 EMA 在有線程進行第一次有效對象分配的時候進行採集,在 TLAB 初始化的時候讀取這個值計算 TLAB 指望大小。
  3. 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 所示。

image

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 作一些操做。

image

8.3.1. GC 前的操做

在 GC 前,若是啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB 關閉),則須要將全部線程的 TLAB 填充 dummy Object 退還給堆,並計算並採樣一些東西用於之後的 TLAB 大小計算。

首先爲了保證本次計算具備參考意義,須要先判斷是否堆上 TLAB 空間被用了一半以上,假設不足,那麼認爲本輪 GC 的數據沒有參考意義。若是被用了一半以上,那麼計算新的分配比例,新的分配比例 = 線程本輪 GC 分配空間的大小 / 堆上全部線程 TLAB 使用的空間,這麼計算主要由於分配比例描述的是當前線程佔用堆上全部給 TLAB 的空間的比例,每一個線程不同,經過這個比例動態控制不一樣業務線程的 TLAB 大小。

線程本輪 GC 分配空間的大小包含 TLAB 中分配的和 TLAB 外分配的,從 圖八、圖九、圖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 方面的緣由。

相關文章
相關標籤/搜索