今天,又是乾貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。因爲文章很長,每一個人閱讀習慣不一樣,因此特此拆成單篇版和多篇版git
1. 觀前提醒
本期內容比較硬核,很是全面,涉及到了設計思想到實現原理以及源碼,而且還給出了相應的日誌以及監控方式,若是有不清楚或者有疑問的地方,歡迎留言。github
其中涉及到的設計思想主要爲我的理解,實現原理以及源碼解析也是我的整理,若是有不許確的地方,很是歡迎指正!提早感謝~~算法
咱們常常會 new 一個對象,這個對象是須要佔用空間的,第一次 new 一個對象佔用的空間如 圖00 所示,數組
咱們這裏先只關心堆內部的存儲,元空間中的存儲,咱們會在另外一個系列詳細討論。堆內部的存儲包括對象頭,對象體以及內存對齊填充,那麼這塊空間是如何分配的呢?緩存
首先,對象所需的內存,在對象的類被解析加載進入元空間以後,就能夠在分配內存建立前計算出來。假設如今咱們本身來設計堆內存分配,一種最簡單的實現方式就是線性分配,也被稱爲撞針分配(bump-the-pointer)。安全
每次須要分配內存時,先計算出須要的內存大小,而後 CAS 更新如 圖01 中所示的內存分配指針,標記分配的內存。可是內存通常不是這麼整齊的,可能有些內存在分配有些內存就被釋放回收了。因此通常不會只靠撞針分配。一種思路是在撞針分配的基礎上,加上一個 FreeList。多線程
簡單的實現是將釋放的對象內存加入 FreeList,下次分配對象的時候,優先從 FreeList 中尋找合適的內存大小進行分配,以後再在主內存中撞針分配。ide
這樣雖然必定程度上解決了問題,可是目前大多數應用是多線程的,因此內存分配是多線程的,都從主內存中分配,CAS 更新重試過於頻繁致使效率低下。目前的應用,通常根據不一樣業務區分了不一樣的線程池,在這種狀況下,通常每一個線程分配內存的特性是比較穩定的。這裏的比較穩定指的是,每次分配對象的大小,每輪 GC 分配區間內的分配對象的個數以及總大小。因此,咱們能夠考慮每一個線程分配內存後,就將這塊內存保留起來,用於下次分配,這樣就不用每次從主內存中分配了。若是能估算每輪 GC 內每一個線程使用的內存大小,則能夠提早分配好內存給線程,這樣就更能提升分配效率。這種內存分配的實現方式,在 JVM 中就是 TLAB (Thread Local Allocate Buffer)。函數
咱們這裏不考慮棧上分配,這些會在 JIT 的章節詳細分析,咱們這裏考慮的是沒法棧上分配須要共享的對象。工具
對於 HotSpot JVM 實現,全部的 GC 算法的實現都是一種對於堆內存的管理,也就是都實現了一種堆的抽象,它們都實現了接口 CollectedHeap。當分配一個對象堆內存空間時,在 CollectedHeap 上首先都會檢查是否啓用了 TLAB,若是啓用了,則會嘗試 TLAB 分配;若是當前線程的 TLAB 大小足夠,那麼從線程當前的 TLAB 中分配;若是不夠,可是當前 TLAB 剩餘空間小於最大浪費空間限制(這是一個動態的值,咱們後面會詳細分析),則從堆上(通常是 Eden 區) 從新申請一個新的 TLAB 進行分配。不然,直接在 TLAB 外進行分配。TLAB 外的分配策略,不一樣的 GC 算法不一樣。例如G1:
TLAB 是線程私有的,線程初始化的時候,會建立並初始化 TLAB。同時,在 GC 掃描對象發生以後,線程第一次嘗試分配對象的時候,也會建立並初始化 TLAB。
TLAB 生命週期中止(TLAB 聲明週期中止不表明內存被回收,只是表明這個 TLAB 再也不被這個線程私有管理)在:
TLAB 要解決的問題很明顯,儘可能避免從堆上直接分配內存從而避免頻繁的鎖爭用。
引入 TLAB 以後,TLAB 的設計上,也有不少值得考慮的問題。
出現孔隙的狀況:
若是無論這些孔隙,因爲 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,若是不填充的話,外部並不知道哪一部分被使用哪一部分沒有,須要作額外的檢查,那麼會影響 GC 掃描效率。因此 TLAB 迴歸 Eden 的時候,會將剩餘可用的空間用一個 dummy object 填充滿。若是填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記以後跳過這塊內存,增長掃描效率。可是同時,因爲須要填充這個 dummy object,因此須要預留出這個對象的對象頭的空間。
若是咱們能提早知道在這一輪內每一個線程會分配多少內存,那麼咱們能夠直接提早分配好。可是,這簡直是癡人說夢。每一個線程在每一輪 GC 的分配狀況可能都是不同的:
因此,綜合考慮以上狀況,咱們應該這麼實現 TLAB:
在上面提到的 TLAB 大小設計的時候,咱們常常提到指望。這個指望是根據歷史數據計算得出的,也就是每次輸入採樣值,根據歷史採樣值得出最新的指望值。不只 TLAB 用到了這種指望計算,GC 和 JIT 等等 JVM 機制中都用到了。這裏咱們來看一種 TLAB 中常常用到的 EMA(Exponential Moving Average 指數平均數) 算法:
EMA 算法的核心在於設置合適的最小權重,咱們假設一個場景:首先採樣100個 100(算法中的前 100 個是爲了排除不穩定的干擾,咱們這裏直接忽略前 100 個採樣),以後採樣 50 個 2,最後採樣 50 個 200,對於不一樣的最小權重,來看一下變化曲線。
能夠看出,最小權重越大,變化得越快,受歷史數據影響越小。根據應用設置合適的最小權重,可讓你的指望更加理想。
這塊對應的源代碼:gcUtil.hpp
的 AdaptiveWeightedAverage
類。
這裏僅僅是列出來,並附上簡介,看不懂不要緊,以後會有詳細分析,幫助你理解每個參數。等你理解後,這個小章節就是你的工具書啦~~
如下參數以及默認值基於 OpenJDK 17
從 Java 12 開始已過時,目前已經沒有相關的邏輯了。以前是用於 TLAB 統計數據從而更好地伸縮 TLAB 可是性能消耗相對較大,可是如今主要經過 EMA 計算了。
說明:是否啓用 TLAB,默認是啓用的。
默認:true
舉例:若是想關閉:-XX:-UseTLAB
說明:是否將新建立的 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
說明:TLAB 是不是可變的,默認爲是,也就是會根據線程歷史分配數據相關 EMA 計算出每次指望 TLAB 大小並以這個大小爲準申請 TLAB。
默認:true
舉例:若是想關閉:-XX:-ResizeTLAB
說明:初始 TLAB 大小。單位是字節
默認:0, 0 就是不主動設置 TLAB 初始大小,而是經過 JVM 本身計算每個線程的初始大小
舉例:-XX:TLABSize=65536
說明:最小 TLAB 大小。單位是字節
默認:2048
舉例:-XX:TLABSize=4096
說明: TLAB 初始大小計算和線程數量有關,可是線程是動態建立銷燬的。因此須要基於歷史線程個數推測接下來的線程個數來計算 TLAB 大小。通常 JVM 內像這種預測函數都採用了 EMA 。這個參數就是 圖06 中的最小權重,權重越高,最近的數據佔比影響越大。TLAB 從新計算大小是根據分配比例,分配比例也是採用了 EMA 算法,最小權重也是 TLABAllocationWeight
默認:35
舉例:-XX:TLABAllocationWeight=70
說明:TLAB 的大小計算涉及到了 Eden 區的大小以及能夠浪費的比率。TLAB 浪費指的是上面提到的從新申請新的 TLAB 的時候老的 TLAB 沒有分配的空間。這個參數其實就是 TLAB 浪費佔用 Eden 的百分比,這個參數的做用會在接下來的原理說明內詳細說明
默認:1
舉例:-XX:TLABWasteTargetPercent=10
說明: 初始最大浪費空間限制計算參數,初始最大浪費空間限制 = 當前指望 TLAB 大小 / TLABRefillWasteFraction
默認:64
舉例:-XX:TLABRefillWasteFraction=32
說明: 最大浪費空間限制並非不變的,在發生 TLAB 緩慢分配的時候(也就是當前 TLAB 空間不足以分配的時候),會增長最大浪費空間限制。這個參數就是 TLAB 緩慢分配時容許的 TLAB 浪費增量。單位不是字節,而是 MarkWord
個數,也就是 Java 堆的內存最小單元,64 位虛擬機的狀況下,MarkWord
大小爲 3 字節。
默認:4
舉例:-XX:TLABWasteIncrement=4
以前咱們提到了引入 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 的整個生命週期的每一個流程。
線程初始化的時候,若是 JVM 啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB
關閉),則會初始化 TLAB,在發生對象分配時,會根據指望大小申請 TLAB 內存。同時,在 GC 掃描對象發生以後,線程第一次嘗試分配對象的時候,也會從新申請 TLAB 內存。咱們先只關心初始化,初始化的流程圖如 圖08 所示:
初始化時候會計算 TLAB 初始指望大小。這涉及到了 TLAB 大小的限制:
MinTLABSize
指定以後的流程裏面,不管什麼時候,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍內,爲了不囉嗦,咱們不會再強調這個限制~~~!!! 以後的流程裏面,不管什麼時候,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍內,爲了不囉嗦,咱們不會再強調這個限制~~~!!! 以後的流程裏面,不管什麼時候,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍內,爲了不囉嗦,咱們不會再強調這個限制~~~!!! 重要的事情說三遍~
TLAB 指望大小(desired size) 在初始化的時候會計算 TLAB 指望大小,以後再 GC 等操做回收掉 TLAB 須要重計算這個指望大小。根據這個指望大小,TLAB 在申請空間的時候每次申請都會以這個指望大小做爲基準的空間做爲 TLAB 分配空間。
如 圖08 所示,若是指定了 TLABSize,就用這個大小做爲初始指望大小。若是沒有指定,則按照以下的公式進行計算:
堆給TLAB的空間總大小
/(當前有效分配線程個數指望
*重填次數配置
)
如 圖08 所示,接下來會計算TLAB 初始分配比例。
線程私有分配比例 EMA:與有效分配線程個數 EMA對應,有效分配線程個數 EMA是對於全局來講,每一個線程應該佔用多大的 TLAB 的描述,而分配比例 EMA 至關於對於當前線程應該佔用的總 TLAB 空間的大小的一種動態控制。
初始化的時候,分配比例其實就是等於 1/當前有效分配線程個數
。圖08 的公式,代入以前的計算 TLAB 指望大小的公式,消參簡化以後就是1/當前有效分配線程個數
。這個值做爲初始值,採集如線程私有的分配比例 EMA。
這些採集數據會用於以後的當前線程的分配比例的計算與採集,從而影響以後的當前線程 TLAB 指望大小。
TLAB 分配流程如 圖09 所示。
若是啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB
關閉),則首先從線程當前 TLAB 分配內存,若是分配成功則返回,不然根據當前 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 以後,根據 ZeroTLAB 配置,決定是否將每一個字節賦 0。在建立對象的時候,原本也要對每一個字段賦初始值,大部分字段初始值都是 0,而且,在 TLAB 返還到堆時,剩餘空間填充的也是 int[] 數組,裏面都是 0。因此其實能夠提早填充好。而且,TLAB 剛分配出來的時候,賦 0 也能利用好 Allocation prefetch 的機制適應 CPU 緩存行(Allocation prefetch 的機制會在另外一個系列說明),因此能夠經過打開 ZeroTLAB 來在分配 TLAB 空間以後馬上賦 0。
直接從堆上分配是最慢的分配方式。一種狀況就是,若是當前 TLAB 剩餘空間大於當前最大浪費空間限制,直接在堆上分配。而且,還會增長當前最大浪費空間限制,每次有這樣的分配就會增長 TLABWasteIncrement 的大小,這樣在必定次數的直接堆上分配以後,當前最大浪費空間限制一直增大會致使當前 TLAB 剩餘空間小於當前最大浪費空間限制,從而申請新的 TLAB 進行分配。
相關流程如 圖10 所示,在 GC 前與 GC 後,都會對 TLAB 作一些操做。
在 GC 前,若是啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB
關閉),則須要將全部線程的 TLAB 填充 dummy Object 退還給堆,並計算並採樣一些東西用於之後的 TLAB 大小計算。
首先爲了保證本次計算具備參考意義,須要先判斷是否堆上 TLAB 空間被用了一半以上,假設不足,那麼認爲本輪 GC 的數據沒有參考意義。若是被用了一半以上,那麼計算新的分配比例,新的分配比例 = 線程本輪 GC 分配空間的大小 / 堆上全部線程 TLAB 使用的空間,這麼計算主要由於分配比例描述的是當前線程佔用堆上全部給 TLAB 的空間的比例,每一個線程不同,經過這個比例動態控制不一樣業務線程的 TLAB 大小。
線程本輪 GC 分配空間的大小包含 TLAB 中分配的和 TLAB 外分配的,從 圖八、圖九、圖10 流程圖中對於線程記錄中的線程分配空間大小的記錄就能看出,讀取出線程分配空間大小減去上一輪 GC 結束時線程分配空間大小就是線程本輪 GC 分配空間的大小。
最後,將當前 TLAB 填充好 dummy object 以後,返還給堆。
若是啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB
關閉),以及 TLAB 大小可變(默認是啓用的, 能夠經過 -XX:-ResizeTLAB
關閉),那麼在 GC 後會從新計算每一個線程 TLAB 的指望大小,*新的指望大小 = 堆給TLAB的空間總大小 當前分配比例 EMA / 重填次數配置。而後會重置最大浪費空間限制,爲當前 指望大小 / TLABRefillWasteFraction**。
若是這裏看的比較吃力,能夠直接看第 10 章,熱門 Q&A,裏面有不少你們常問的問題
線程初始化的時候,若是 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,這裏不用關心
首先是 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(); }
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(); }
不一樣的 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 的空區域有關。這個緣由以前已經說明了。
當 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); }
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; }
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; }
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; }
從新計算會取 當前堆剩餘給 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; }
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); } }
不一樣的 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(); }
不一樣的 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()); }
這裏我會持續更新的,解決你們的各類疑問
主要保證 GC 的時候掃描高效。因爲 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,若是不填充的話,外部並不知道哪一部分被使用哪一部分沒有,須要作額外的檢查,若是填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記以後跳過這塊內存,增長掃描效率。反正這塊內存已經屬於 TLAB,其餘線程在下次掃描結束前是沒法使用的。這個 dummy object 就是 int 數組。爲了必定能有填充 dummy object 的空間,通常 TLAB 大小都會預留一個 dummy object 的 header 的空間,也是一個 int[]
的 header,因此 TLAB 的大小不能超過int 數組的最大大小,不然沒法用 dummy object 填滿未使用的空間。
當從新分配一個 TLAB 的時候,原有的 TLAB 可能還有空間剩餘。原有的 TLAB 被退回堆以前,須要填充好 dummy object。這樣致使這塊內存沒法分配對象,所示被稱爲「浪費」。若是不限制,遇到 TLAB 剩餘空間不足的狀況就會從新申請,致使分配效率下降,大部分空間被 dummy object 佔滿了,致使 GC 更加頻繁。
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 次。
當分配出來 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 的機制詳情會在另外一個系列說明)
根據以前的分析,每一個線程的 TLAB 的大小,會根據線程分配的特性,不斷變化並趨於穩定,大小主要是由分配比例 EMA 決定,可是這個採集是須要必定運行次數的。而且 EMA 的前 100 次採集默認是不夠穩定的,因此 TLAB 大小也在程序一開始的時候變化頻繁。當程序線程趨於穩定,運行一段時間後, 每一個線程 TLAB 大小也會趨於穩定而且調整到最適合這個線程對象分配特性的大小。這樣,就更接近最理想的只有 Eden 區滿了纔會 GC,全部 Eden 區的對象都是經過 TLAB 分配的高效分配狀況。這就是 Java 代碼越執行越快在 TLAB 方面的緣由。