什么是 TLAB?
TLAB(Thread Local Allocation Buffer)線程本地分配緩存區(qū)亿扁,這是一個線程專用的內(nèi)存分配區(qū)域如蚜。既然是一個內(nèi)存分配區(qū)域,我們就先要搞清楚 Java 內(nèi)存大概是如何分配的挡逼。
我們一般認(rèn)為 Java 中 new 的對象都是在堆上分配括改,這個說法不夠準(zhǔn)確,應(yīng)該是大部分對象在堆上的TLAB分配家坎,還有一部分在棧上分配或者是堆上直接分配嘱能,可能 Eden 區(qū)也可能年老代。同時虱疏,對于一些的 GC 算法惹骂,還可能直接在老年代上面分配,例如 G1 GC 中的humongous allocations(大對象分配)订框,就是對象在超過 Region 一半大小的時候析苫,直接在老年代的連續(xù)空間分配。
這里穿扳,我們先只關(guān)心 TLAB 分配衩侥。 對于單線程應(yīng)用,每次分配內(nèi)存矛物,會記錄上次分配對象內(nèi)存地址末尾的指針茫死,之后分配對象會從這個指針開始檢索分配。這個機制叫做bump-the-pointer(撞針)履羞。 對于多線程應(yīng)用來說峦萎,內(nèi)存分配需要考慮線程安全屡久。最直接的想法就是通過全局鎖,但是這個性能會很差爱榔。為了優(yōu)化這個性能被环,我們考慮可以每個線程分配一個線程本地私有的內(nèi)存池,然后采用bump-the-pointer機制進行內(nèi)存分配详幽。這個線程本地私有的內(nèi)存池筛欢,就是 TLAB。只有 TLAB 滿了唇聘,再去申請內(nèi)存的時候版姑,需要擴充 TLAB 或者使用新的 TLAB,這時候才需要鎖迟郎。這樣大大減少了鎖使用剥险。
TLAB 相關(guān) JVM 參數(shù)詳解
我們先來瀏覽下 TLAB 相關(guān)的 JVM 參數(shù)以及其含義,在下一小節(jié)會深入源碼分析原理以及設(shè)計這個參數(shù)是為何宪肖。
以下參數(shù)與默認(rèn)值均來自于 OpenJDK 11
1. UseTLAB
說明:是否啟用 TLAB表制,默認(rèn)是啟用的。
默認(rèn):true
舉例:如果想關(guān)閉:-XX:-UseTLAB
2. ResizeTLAB
說明:TLAB 是否是自適應(yīng)可變的匈庭,默認(rèn)為是夫凸。
默認(rèn):true
舉例:如果想關(guān)閉:-XX:-ResizeTLAB
3. TLABSize
說明:初始 TLAB 大小。單位是字節(jié)
默認(rèn):0阱持, 0 就是不主動設(shè)置 TLAB 初始大小,而是通過 JVM 自己計算每一個線程的初始大小
舉例:-XX:TLABSize=65536
4. MinTLABSize
說明:最小 TLAB 大小魔熏。單位是字節(jié)
默認(rèn):2048
舉例:-XX:TLABSize=4096
5. TLABWasteTargetPercent
說明:TLAB 的大小計算涉及到了 Eden 區(qū)的大小以及可以浪費的比率衷咽。TLAB 浪費占用 Eden 的百分比,這個參數(shù)的作用會在接下來的原理說明內(nèi)詳細說明
默認(rèn):1
舉例:-XX:TLABWasteTargetPercent=10
6. TLABAllocationWeight
說明: TLAB 大小計算和線程數(shù)量有關(guān)蒜绽,但是線程是動態(tài)創(chuàng)建銷毀的镶骗。所以需要基于歷史線程個數(shù)推測接下來的線程個數(shù)來計算 TLAB 大小。一般 JVM 內(nèi)像這種預(yù)測函數(shù)都采用了 EMA (Exponential Moving Average 指數(shù)平均數(shù))算法進行預(yù)測躲雅,會在接下來的原理說明內(nèi)詳細說明鼎姊。這個參數(shù)代表權(quán)重,權(quán)重越高相赁,最近的數(shù)據(jù)占比影響越大相寇。
默認(rèn):35
舉例:-XX:TLABAllocationWeight=70
7. TLABRefillWasteFraction
說明: 在一次 TLAB 再填充(refill)發(fā)生的時候,最大的 TLAB 浪費钮科。至于什么是再填充(refill)唤衫,什么是 TLAB 浪費,會在接下來的原理說明內(nèi)詳細說明
默認(rèn):64
舉例:-XX:TLABRefillWasteFraction=32
8. TLABWasteIncrement
說明: TLAB 緩慢分配時允許的 TLAB 浪費增量绵脯,什么是 TLAB 浪費佳励,什么是 TLAB 緩慢分配休里,會在接下來的原理說明內(nèi)詳細說明。單位不是字節(jié)赃承,而是MarkWord個數(shù)妙黍,也就是 Java 堆的內(nèi)存最小單元
默認(rèn):4
舉例:-XX:TLABWasteIncrement=4
9. ZeroTLAB
說明: 是否將新創(chuàng)建的 TLAB 內(nèi)的對象所有字段歸零
默認(rèn):false
舉例:-XX:+ZeroTLAB
TLAB 生命周期與原理詳解
TLAB 是從堆上 Eden 區(qū)的分配的一塊線程本地私有內(nèi)存。線程初始化的時候瞧剖,如果 JVM 啟用了 TLAB(默認(rèn)是啟用的拭嫁, 可以通過-XX:-UseTLAB關(guān)閉),則會創(chuàng)建并初始化 TLAB筒繁。同時噩凹,在GC 掃描對象發(fā)生之后,線程第一次嘗試分配對象的時候毡咏,也會創(chuàng)建并初始化 TLAB 驮宴。在 TLAB 已經(jīng)滿了或者接近于滿了的時候,TLAB可能會被釋放回 Eden呕缭。GC 掃描對象發(fā)生時堵泽,TLAB 會被釋放回 Eden。TLAB 的生命周期期望只存在于一個 GC 掃描周期內(nèi)恢总。在 JVM 中迎罗,一個 GC 掃描周期,就是一個epoch片仿。那么纹安,可以知道,TLAB 內(nèi)分配內(nèi)存一定是線性分配的砂豌。
TLAB 的最小大小:通過MinTLABSize指定
TLAB 的最大大小:不同的 GC 中不同厢岂,G1 GC 中為大對象(humongous object)大小,也就是 G1 region 大小的一半阳距。因為開頭提到過塔粒,在 G1 GC 中,大對象不能在 TLAB 分配筐摘,而是老年代卒茬。ZGC 中為頁大小的 8 分之一,類似的在大部分情況下 Shenandoah GC 也是每個 Region 大小的 8 分之一咖熟。他們都是期望至少有 8 分之 7 的區(qū)域是不用退回的減少選擇 Cset 的時候的掃描復(fù)雜度圃酵。對于其他的 GC,則是 int 數(shù)組的最大大小球恤,這個和為了填充 dummy object 表示 TLAB 的空區(qū)域有關(guān)辜昵。
為何要填充 dummy object?
由于 TLAB 僅線程內(nèi)知道哪些被分配了咽斧,在 GC 掃描發(fā)生時返回 Eden 區(qū)堪置,如果不填充的話躬存,外部并不知道哪一部分被使用哪一部分沒有,需要做額外的檢查舀锨,如果填充已經(jīng)確認(rèn)會被回收的對象岭洲,也就是 dummy object, GC 會直接標(biāo)記之后跳過這塊內(nèi)存坎匿,增加掃描效率盾剩。反正這塊內(nèi)存已經(jīng)屬于 TLAB,其他線程在下次掃描結(jié)束前是無法使用的替蔬。這個 dummy object 就是 int 數(shù)組告私。為了一定能有填充 dummy object 的空間,一般 TLAB 大小都會預(yù)留一個 dummy object 的 header 的空間承桥,也是一個int[]的 header驻粟,所以 TLAB 的大小不能超過int 數(shù)組的最大大小,否則無法用 dummy object 填滿未使用的空間凶异。
TLAB 的大小: 如果指定了TLABSize蜀撑,就用這個大小作為初始大小。如果沒有指定剩彬,則按照如下的公式進行計算:Eden 區(qū)大小 / (當(dāng)前 epcoh 內(nèi)會分配對象期望線程個數(shù) * 每個 epoch 內(nèi)每個線程 refill 次數(shù)配置)
當(dāng)前 epcoh 內(nèi)會分配對象期望線程個數(shù)酷麦,也就是會創(chuàng)建并初始化 TLAB 的線程個數(shù),這個從之前提到的 EMA (Exponential Moving Average 指數(shù)平均數(shù))算法采集預(yù)測而來喉恋。算法是:
采樣次數(shù)小于等于 100 時沃饶,每次采樣:1. 次數(shù)權(quán)重 = 100 / 次數(shù)2. 計算權(quán)重 = 次數(shù)權(quán)重 與 TLABAllocationWeight 中大的那個3. 新的平均值 = (100% - 計算權(quán)重%) * 之前的平均值 + 計算權(quán)重% * 當(dāng)前采樣值采樣次數(shù)大于 100 時,每次采樣:新的平均值 = (100% - TLABAllocationWeight %) * 之前的平均值 + TLABAllocationWeight % * 當(dāng)前采樣值復(fù)制代碼
可以看出TLABAllocationWeight越大轻黑,則最近的線程數(shù)量對于這個下個 epcoh 內(nèi)會分配對象期望線程個數(shù)影響越大绍坝。
每個 epoch 內(nèi)期望 refill 次數(shù)就是在每個 GC 掃描周期內(nèi),refill 的次數(shù)苔悦。那么什么是 refill 呢?
在 TLAB 內(nèi)存充足的時候分配對象就是快分配椎咧,否則在 TLAB 內(nèi)存不足的時候分配對象就是慢分配玖详,慢分配可能會發(fā)生兩種處理:
1.線程獲取新的 TLAB。老的 TLAB 回歸 Eden勤讽,之后線程獲取新的 TLAB 分配對象蟋座。
2.對象在 TLAB 外分配,也就 Eden 區(qū)脚牍。
這兩種處理主要由TLAB最大浪費空間決定向臀,這是一個動態(tài)值。初始TLAB最大浪費空間=TLAB 的大小/TLABRefillWasteFraction诸狭。根據(jù)前面提到的這個 JVM 參數(shù)券膀,默認(rèn)為TLAB 的大小的 64 分之一君纫。之后,伴隨著每次慢分配芹彬,這個TLAB最大浪費空間會每次遞增TLABWasteIncrement大小的空間蓄髓。如果當(dāng)前 TLAB 的剩余容量大于TLAB最大浪費空間,就不在當(dāng)前TLAB分配舒帮,直接在 Eden 區(qū)進行分配会喝。如果剩余容量小于TLAB最大浪費空間,就丟棄當(dāng)前 TLAB 回歸 Eden玩郊,線程獲取新的 TLAB 分配對象肢执。refill指的就是這種線程獲取新的 TLAB 分配對象的行為。
那么译红,也就好理解為何要盡量滿足TLAB 的大小=Eden 區(qū)大小 / (下個 epcoh 內(nèi)會分配對象期望線程個數(shù) * 每個 epoch 內(nèi)每個線程 refill 次數(shù)配置)了预茄。盡量讓所有對象在 TLAB 內(nèi)分配,也就是 TLAB 可能要占滿 Eden临庇。在下次 GC 掃描前反璃,refill回 Eden 的內(nèi)存別的線程是不能用的,因為剩余空間已經(jīng)填滿了dummy object假夺。所以所有線程使用內(nèi)存大小就是下個 epcoh 內(nèi)會分配對象期望線程個數(shù) * 每個 epoch 內(nèi)每個線程 refill 次數(shù)配置淮蜈,對象一般都在 Eden 區(qū)由某個線程分配,也就所有線程使用內(nèi)存大小就最好是整個 Eden已卷。但是這種情況太過于理想梧田,總會有內(nèi)存被填充了dummy object而造成了浪費,因為 GC 掃描隨時可能發(fā)生侧蘸。假設(shè)平均下來裁眯,GC 掃描的時候,每個線程當(dāng)前的 TLAB 都有一半的內(nèi)存被浪費讳癌,這個每個線程使用內(nèi)存的浪費的百分比率(也就是TLABWasteTargetPercent)穿稳,也就是等于(注意,僅最新的那個 TLAB 有浪費晌坤,之前refill 退回的假設(shè)是沒有浪費的):
1/2 * (每個 epoch 內(nèi)每個線程期望 refill 次數(shù)) * 100
那么每個 epoch 內(nèi)每個線程 refill 次數(shù)配置就等于50 / TLABWasteTargetPercent逢艘, 默認(rèn)也就是 50 次。
當(dāng)TLABResize設(shè)置為 true 的時候骤菠,在每個epoch當(dāng)線程需要分配對象的時候它改,TLAB 大小都會被重新計算,并用這個最新的大小去從 Eden 申請內(nèi)存商乎。如果沒有對象分配則不重新計算央拖,也不申請(廢話~~~)。主要是為了能讓線程 TLAB 的refill 次數(shù)接近于每個 epoch 內(nèi)每個線程 refill 次數(shù)配置。這樣就能讓浪費比例接近于用戶配置的TLABWasteTargetPercent.這個大小重新計算的公式為:TLAB 最新大小 * EMA refill 次數(shù) / 每個 epoch 內(nèi)每個線程 refill 次數(shù)配置鲜戒。
TLAB 相關(guān)源碼詳解
1. TLAB 類構(gòu)成
線程初始化的時候专控,如果 JVM 啟用了 TLAB(默認(rèn)是啟用的, 可以通過 -XX:-UseTLAB 關(guān)閉)袍啡,則會初始化 TLAB踩官。
TLAB 包括如下幾個 field (HeapWord* 可以理解為堆中的內(nèi)存地址):src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
//靜態(tài)全局變量static size_t? _max_size;? ? ? ? ? ? ? ? ? ? ? ? ? // 所有 TLAB 的最大大小? static int? ? ? _reserve_for_allocation_prefetch;? // CPU 緩存優(yōu)化 Allocation Prefetch 的保留空間,這里先不用關(guān)心? static unsigned _target_refills;? ? ? ? ? ? ? ? ? ? //每個 epoch 周期內(nèi)期望的 refill 次數(shù)//以下是 TLAB 的主要構(gòu)成 fieldHeapWord* _start;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // TLAB 起始地址境输,表示堆內(nèi)存地址都用 HeapWord* HeapWord* _top;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 上次分配的內(nèi)存地址HeapWord* _end;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // TLAB 結(jié)束地址size_t? ? _desired_size;? ? ? ? ? ? ? ? ? ? ? // TLAB 大小 包括保留空間蔗牡,表示內(nèi)存大小都需要通過 size_t 類型,也就是實際字節(jié)數(shù)除以 HeapWordSize 的值size_t? ? _refill_waste_limit;? ? ? ? ? ? ? ? // TLAB最大浪費空間嗅剖,剩余空間不足分配浪費空間限制辩越。在TLAB剩余空間不足的時候,根據(jù)這個值決定分配策略信粮,如果浪費空間大于這個值則直接在 Eden 區(qū)分配黔攒,如果小于這個值則將當(dāng)前 TLAB 放回 Eden 區(qū)管理并從 Eden 申請新的 TLAB 進行分配。 AdaptiveWeightedAverage _allocation_fraction;? // 當(dāng)前 TLAB 占用所有TLAB最大空間(一般是Eden大星吭怠)的期望比例督惰,通過 EMA 算法采集預(yù)測//以下是我們這里不用太關(guān)心的 fieldHeapWord* _allocation_end;? ? ? ? ? ? ? ? ? ? // TLAB 真正可以用來分配內(nèi)存的結(jié)束地址,這個是 _end 結(jié)束地址排除保留空間旅掂,至于為何需要保留空間我們這里先不用關(guān)心赏胚,稍后我們會解釋這個參數(shù)HeapWord* _pf_top;? ? ? ? ? ? ? ? ? ? ? ? ? ? // Allocation Prefetch CPU 緩存優(yōu)化機制相關(guān)需要的參數(shù),這里先不用考慮size_t? ? _allocated_before_last_gc;? ? ? ? ? // GC統(tǒng)計數(shù)據(jù)采集相關(guān)商虐,例如線程內(nèi)存申請數(shù)據(jù)統(tǒng)計等等觉阅,這里先不用關(guān)心unsigned? _number_of_refills;? ? ? ? ? ? ? ? // 線程分配內(nèi)存數(shù)據(jù)采集相關(guān),TLAB 剩余空間不足分配次數(shù)unsigned? _fast_refill_waste;? ? ? ? ? ? ? ? // 線程分配內(nèi)存數(shù)據(jù)采集相關(guān)秘车,TLAB 快速分配浪費典勇,什么是快速分配,待會會說到unsigned? _slow_refill_waste;? ? ? ? ? ? ? ? // 線程分配內(nèi)存數(shù)據(jù)采集相關(guān)叮趴,TLAB 慢速分配浪費割笙,什么是慢速分配,待會會說到unsigned? _gc_waste;? ? ? ? ? ? ? ? ? ? ? ? ? // 線程分配內(nèi)存數(shù)據(jù)采集相關(guān)眯亦,gc浪費unsigned? _slow_allocations;? ? ? ? ? ? ? ? ? // 線程分配內(nèi)存數(shù)據(jù)采集相關(guān)咳蔚,TLAB 慢速分配計數(shù) size_t? ? _allocated_size;? ? ? ? ? ? ? ? ? ? //分配的內(nèi)存大小size_t? ? _bytes_since_last_sample_point;? ? // JVM TI 采集指標(biāo)相關(guān) field,這里不用關(guān)心復(fù)制代碼
2. TLAB 初始化
首先是 JVM 啟動的時候搔驼,全局 TLAB 需要初始化:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::startup_initialization() {? //初始化,也就是歸零統(tǒng)計數(shù)據(jù)? ThreadLocalAllocStats::initialize();? // 假設(shè)平均下來侈询,GC 掃描的時候舌涨,每個線程當(dāng)前的 TLAB 都有一半的內(nèi)存被浪費,這個每個線程使用內(nèi)存的浪費的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意囊嘉,僅最新的那個 TLAB 有浪費温技,之前 refill 退回的假設(shè)是沒有浪費的):1/2 * (每個 epoch 內(nèi)每個線程期望 refill 次數(shù)) * 100? //那么每個 epoch 內(nèi)每個線程 refill 次數(shù)配置就等于 50 / TLABWasteTargetPercent, 默認(rèn)也就是 50 次扭粱。? _target_refills = 100 / (2 * TLABWasteTargetPercent);? // 但是初始的 _target_refills 需要設(shè)置最多不超過 2 次來減少 VM 初始化時候 GC 的可能性? _target_refills = MAX2(_target_refills, 2U);//如果 C2 JIT 編譯存在并啟用舵鳞,則保留 CPU 緩存優(yōu)化 Allocation Prefetch 空間,這個這里先不用關(guān)心琢蛤,會在別的章節(jié)講述#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());}復(fù)制代碼
每個線程維護自己的 TLAB蜓堕,同時每個線程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小博其,線程數(shù)量套才,還有線程的對象分配速率決定。 在 Java 線程開始運行時慕淡,會先分配 TLAB:src/hotspot/share/runtime/thread.cpp
void JavaThread::run() {? // initialize thread-local alloc buffer related fields? this->initialize_tlab();? //剩余代碼忽略}復(fù)制代碼
分配 TLAB 其實就是調(diào)用 ThreadLocalAllocBuffer 的 initialize 方法背伴。src/hotspot/share/runtime/thread.hpp
void initialize_tlab() {? ? //如果沒有通過 -XX:-UseTLAB 禁用 TLAB,則初始化TLAB? ? if (UseTLAB) {? ? ? tlab().initialize();? ? }}// Thread-Local Allocation Buffer (TLAB) supportThreadLocalAllocBuffer& tlab()? ? ? ? ? ? ? ? {? return _tlab; }ThreadLocalAllocBuffer _tlab;復(fù)制代碼
ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我們要關(guān)心的各種 field:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::initialize() {? //設(shè)置初始指針峰髓,由于還沒有從 Eden 分配內(nèi)存傻寂,所以這里都設(shè)置為 NULL? initialize(NULL,? ? ? ? ? ? ? ? ? ? // start? ? ? ? ? ? NULL,? ? ? ? ? ? ? ? ? ? // top? ? ? ? ? ? NULL);? ? ? ? ? ? ? ? ? // end? //計算初始期望大小,并設(shè)置? set_desired_size(initial_desired_size());? //所有 TLAB 總大小携兵,不同的 GC 實現(xiàn)有不同的 TLAB 容量疾掰, 一般是 Eden 區(qū)大小? //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes眉孩,可以理解為年輕代減去Survivor區(qū)个绍,也就是Eden區(qū)? size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;? //計算這個線程的 TLAB 期望占用所有 TLAB 總體大小比例? //TLAB 期望占用大小也就是這個 TLAB 大小乘以期望 refill 的次數(shù)? float alloc_frac = desired_size() * target_refills() / (float) capacity;? //記錄下來,用于計算 EMA? _allocation_fraction.sample(alloc_frac);? //計算初始 refill 最大浪費空間浪汪,并設(shè)置? //如前面原理部分所述巴柿,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction? set_refill_waste_limit(initial_refill_waste_limit());? //重置統(tǒng)計? reset_statistics();}復(fù)制代碼
2.1. 初始期望大小是如何計算的呢死遭?
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
//計算初始大小size_t ThreadLocalAllocBuffer::initial_desired_size() {? size_t init_sz = 0;? //如果通過 -XX:TLABSize 設(shè)置了 TLAB 大小广恢,則用這個值作為初始期望大小? //表示堆內(nèi)存占用大小都需要用占用幾個 HeapWord 表示,所以用TLABSize / HeapWordSize? if (TLABSize > 0) {? ? init_sz = TLABSize / HeapWordSize;? } else {? ? //獲取當(dāng)前epoch內(nèi)線程數(shù)量期望呀潭,這個如之前所述通過 EMA 預(yù)測? ? unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();? ? //不同的 GC 實現(xiàn)有不同的 TLAB 容量钉迷,Universe::heap()->tlab_capacity(thread()) 一般是 Eden 區(qū)大小? ? //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes钠署,可以理解為年輕代減去Survivor區(qū)糠聪,也就是Eden區(qū)? ? //整體大小等于 Eden區(qū)大小/(當(dāng)前 epcoh 內(nèi)會分配對象期望線程個數(shù) * 每個 epoch 內(nèi)每個線程 refill 次數(shù)配置)? ? //target_refills已經(jīng)在 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身害,我們會在其他章節(jié)詳細分析)味悄。static size_t min_size()? ? ? ? ? ? ? ? ? ? ? {? ? return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve(); }復(fù)制代碼
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);}復(fù)制代碼
ZGC 中為頁大小的 8 分之一,類似的在大部分情況下 Shenandoah GC 也是每個 Region 大小的 8 分之一丙猬。他們都是期望至少有 8 分之 7 的區(qū)域是不用退回的減少選擇 Cset 的時候的掃描復(fù)雜度:src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp
MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);復(fù)制代碼
src/hotspot/share/gc/z/zHeap.cpp
const size_t? ? ? ZObjectSizeLimitSmall? ? ? ? = ZPageSizeSmall / 8;復(fù)制代碼
對于其他的 GC涨颜,則是 int 數(shù)組的最大大小,這個和為了填充 dummy object 表示 TLAB 的空區(qū)域有關(guān)淮悼。這個原因之前已經(jīng)說明了咐低。
3. TLAB 分配內(nèi)存
當(dāng) new 一個對象時,需要調(diào)用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;}復(fù)制代碼
其核心就是heap()->obj_allocate(this, size, CHECK_NULL)從堆上面分配內(nèi)存: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();}復(fù)制代碼
使用全局的ObjAllocator實現(xiàn)進行對象內(nèi)存分配:src/hotspot/share/gc/shared/memAllocator.cpp
oop MemAllocator::allocate() const {? oop obj = NULL;? {? ? Allocation allocation(*this, &obj);? ? //分配堆內(nèi)存袜腥,繼續(xù)看下面一個方法? ? 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 分配,分配代碼繼續(xù)看下面一個方法? 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");? //從當(dāng)前線程的 TLAB 分配內(nèi)存羹令,TLAB 快分配? HeapWord* mem = _thread->tlab().allocate(_word_size);? //如果沒有分配失敗則返回? if (mem != NULL) {? ? return mem;? }? //如果分配失敗則走 TLAB 慢分配鲤屡,需要 refill 或者直接從 Eden 分配? return allocate_inside_tlab_slow(allocation);}復(fù)制代碼
3.1. TLAB 快分配
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {? //驗證各個內(nèi)存指針有效,也就是 _top 在 _start 和 _end 范圍內(nèi)? invariants();? HeapWord* obj = top();? //如果空間足夠福侈,則分配內(nèi)存? if (pointer_delta(end(), obj) >= size) {? ? set_top(obj + size);? ? invariants();? ? return obj;? }? return NULL;}復(fù)制代碼
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 區(qū)? 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 參數(shù)肪凛,則將對象所有字段置零值? if (ZeroTLAB) {? ? // ..and clear it.? ? Copy::zero_to_words(mem, allocation._allocated_tlab_size);? } else {? ? // ...and zap just allocated object.? }? //設(shè)置新的 TLAB 空間為當(dāng)前線程的 TLAB? tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);? //返回分配的對象內(nèi)存地址? return mem;}復(fù)制代碼
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; }復(fù)制代碼
每次慢分配堰汉,調(diào)用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 參數(shù) TLABWasteIncrementstatic size_t refill_waste_limit_increment()? { return TLABWasteIncrement; }復(fù)制代碼
3.2.2. 重新計算 TLAB 大小
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp_desired_size是什么時候變得呢戳葵?怎么變得呢就乓?
void ThreadLocalAllocBuffer::resize() {? assert(ResizeTLAB, "Should not call this otherwise");? //根據(jù) _allocation_fraction 這個 EMA 采集得出平均數(shù)乘以Eden區(qū)大小,得出 TLAB 當(dāng)前預(yù)測占用內(nèi)存比例? size_t alloc = (size_t)(_allocation_fraction.average() *? ? ? ? ? ? ? ? ? ? ? ? ? (Universe::heap()->tlab_capacity(thread()) / HeapWordSize));? //除以目標(biāo) refill 次數(shù)就是新的 TLAB 大小拱烁,和初始化時候的結(jié)算方法差不多? 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);? //設(shè)置新的 TLAB 大小? set_desired_size(aligned_new_size);? //重置 TLAB 最大浪費空間? set_refill_waste_limit(initial_refill_waste_limit());}復(fù)制代碼
那是什么時候調(diào)用resize的呢生蚁?一般是每次** GC 完成的時候**。大部分的 GC 都是在gc_epilogue方法里面調(diào)用戏自,將每個線程的 TLAB 均resize掉邦投。
4. TLAB 回收
TLAB 回收就是指線程將當(dāng)前的 TLAB 丟棄回 Eden 區(qū)。TLAB 回收有兩個時機:一個是之前提到的在分配對象時擅笔,剩余 TLAB 空間不足尼摹,在 TLAB 滿但是浪費空間小于最大浪費空間的情況下见芹,回收當(dāng)前的 TLAB 并獲取一個新的。另一個就是在發(fā)生 GC 時蠢涝,其實更準(zhǔn)確的說是在 GC 開始掃描時。不同的 GC 可能實現(xiàn)不一樣阅懦,但是時機是基本一樣的和二,這里以 G1 GC 為例:
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
void G1CollectedHeap::gc_prologue(bool full) {? //省略其他代碼? // Fill TLAB's and such? {? ? Ticks start = Ticks::now();? ? //確保堆內(nèi)存是可以解析的? ? ensure_parsability(true);? ? Tickspan dt = Ticks::now() - start;? ? phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS);? }? //省略其他代碼}復(fù)制代碼
為何要確保堆內(nèi)存是可以解析的呢?這樣有利于更快速的掃描堆上對象耳胎。確保內(nèi)存可以解析里面做了什么呢惯吕?
void CollectedHeap::ensure_parsability(bool retire_tlabs) {? //真正的 GC 肯定發(fā)生在安全點上,這個在后面安全點章節(jié)會詳細說明? 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 其實就是將 ThreadLocalAllocBuffer 的堆內(nèi)存指針 MarkWord 置為 NULL? ? ? ? thread->tlab().retire(&stats);? ? ? } else {? ? ? ? //當(dāng)前如果不回收废登,則將 TLAB 填充 Dummy Object 利于解析? ? ? ? thread->tlab().make_parsable();? ? ? }? ? }? }? stats.publish();}復(fù)制代碼
TLAB 主要流程總結(jié)
JFR 對于 TLAB 的監(jiān)控
根據(jù)上面的原理以及源代碼分析,可以得知 TLAB 是 Eden 區(qū)的一部分郁惜,主要用于線程本地的對象分配堡距。在 TLAB 滿的時候分配對象內(nèi)存,可能會發(fā)生兩種處理:
線程獲取新的 TLAB兆蕉。老的 TLAB 回歸 Eden羽戒,Eden進行管理,之后線程通過新的 TLAB 分配對象虎韵。
對象在 TLAB 外分配易稠,也就 Eden 區(qū)。
對于 線程獲取新的 TLAB 這種處理包蓝,也就是 refill驶社,按照 TLAB 設(shè)計原理,這個是經(jīng)常會發(fā)生的测萎,每個 epoch 內(nèi)可能會都會發(fā)生幾次亡电。但是對象直接在 Eden 區(qū)分配,是我們要避免的绳泉。JFR 對于
JFR 針對這兩種處理有不同的事件可以監(jiān)控逊抡。分別是jdk.ObjectAllocationOutsideTLAB和jdk.ObjectAllocationInNewTLAB。jdk.ObjectAllocationInNewTLAB對應(yīng) refill零酪,這個一般我們沒有監(jiān)控的必要(在你沒有修改默認(rèn)的 TLAB 參數(shù)的前提下)冒嫡,用這個測試并學(xué)習(xí) TLAB 的意義比監(jiān)控的意義更大。jdk.ObjectAllocationOutsideTLAB對應(yīng)對象直接在 Eden 區(qū)分配四苇,是我們需要監(jiān)控的孝凌。至于怎么不影響線上性能安全的監(jiān)控,怎么查看并分析月腋,怎么解決蟀架,以及測試生成這兩個事件瓣赂,會在下一節(jié)詳細分析。