今天,又是乾貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。因爲文章很長,每一個人閱讀習慣不一樣,因此特此拆成單篇版和多篇版git
- 全網最硬核 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
8.TLAB 基本流程
8.0. 如何設計每一個線程的 TLAB 大小
以前咱們提到了引入 TLAB 要面臨的問題以及解決方式,根據這些咱們能夠這麼設計 TLAB。github
首先,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 的大小。fetch
綜上所述,咱們能夠得出這樣一個近似的 TLAB 計算公式:url
每一個線程 TLAB 初始大小 = Eden區大小
/ (線程單個 GC 輪次內最多從 Eden 申請多少次 TLAB
* 當前 GC 分配線程個數 EMA
).net
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 外分配的,從 圖八、圖九、圖10 流程圖中對於線程記錄中的線程分配空間大小的記錄就能看出,讀取出線程分配空間大小減去上一輪 GC 結束時線程分配空間大小就是線程本輪 GC 分配空間的大小。
最後,將當前 TLAB 填充好 dummy object 以後,返還給堆。
8.3.2. GC 後的操做
若是啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB
關閉),以及 TLAB 大小可變(默認是啓用的, 能夠經過 -XX:-ResizeTLAB
關閉),那麼在 GC 後會從新計算每一個線程 TLAB 的指望大小,新的指望大小 = 堆給TLAB的空間總大小 * 當前分配比例 EMA / 重填次數配置。而後會重置最大浪費空間限制,爲當前 指望大小 / TLABRefillWasteFraction。