一篇文章搞定 TLAB 原理

全系列目錄:經過 JFR 與日誌深刻探索 JVM - 總覽篇git

什麼是 TLAB?

TLAB(Thread Local Allocation Buffer)線程本地分配緩存區,這是一個線程專用的內存分配區域。既然是一個內存分配區域,咱們就先要搞清楚 Java 內存大概是如何分配的。github

咱們通常認爲 Java 中 new 的對象都是在堆上分配,這個說法不夠準確,應該是大部分對象在堆上的 TLAB分配,還有一部分在 棧上分配 或者是 堆上直接分配,可能 Eden 區也可能年老代。同時,對於一些的 GC 算法,還可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大對象分配),就是對象在超過 Region 一半大小的時候,直接在老年代的連續空間分配。算法

這裏,咱們先只關心 TLAB 分配。
對於單線程應用,每次分配內存,會記錄上次分配對象內存地址末尾的指針,以後分配對象會從這個指針開始檢索分配。這個機制叫作 bump-the-pointer (撞針)。
對於多線程應用來講,內存分配須要考慮線程安全。最直接的想法就是經過全局鎖,可是這個性能會不好。爲了優化這個性能,咱們考慮能夠每一個線程分配一個線程本地私有的內存池,而後採用 bump-the-pointer 機制進行內存分配。這個線程本地私有的內存池,就是 TLAB。只有 TLAB 滿了,再去申請內存的時候,須要擴充 TLAB 或者使用新的 TLAB,這時候才須要鎖。這樣大大減小了鎖使用。數組

TLAB 相關 JVM 參數詳解

咱們先來瀏覽下 TLAB 相關的 JVM 參數以及其含義,在下一小節會深刻源碼分析原理以及設計這個參數是爲什麼。緩存

如下參數與默認值均來自於 OpenJDK 11安全

1. UseTLAB

說明:是否啓用 TLAB,默認是啓用的。多線程

默認:trueide

舉例:若是想關閉:-XX:-UseTLAB函數

2. ResizeTLAB

說明:TLAB 是不是自適應可變的,默認爲是。oop

默認:true

舉例:若是想關閉:-XX:-ResizeTLAB

3. TLABSize

說明:初始 TLAB 大小。單位是字節

默認:0, 0 就是不主動設置 TLAB 初始大小,而是經過 JVM 本身計算每個線程的初始大小

舉例-XX:TLABSize=65536

4. MinTLABSize

說明:最小 TLAB 大小。單位是字節

默認:2048

舉例-XX:TLABSize=4096

5. TLABWasteTargetPercent

說明:TLAB 的大小計算涉及到了 Eden 區的大小以及能夠浪費的比率。TLAB 浪費佔用 Eden 的百分比,這個參數的做用會在接下來的原理說明內詳細說明

默認:1

舉例-XX:TLABWasteTargetPercent=10

6. TLABAllocationWeight

說明: TLAB 大小計算和線程數量有關,可是線程是動態建立銷燬的。因此須要基於歷史線程個數推測接下來的線程個數來計算 TLAB 大小。通常 JVM 內像這種預測函數都採用了 EMA (Exponential Moving Average 指數平均數)算法進行預測,會在接下來的原理說明內詳細說明。這個參數表明權重,權重越高,最近的數據佔比影響越大。

默認:35

舉例-XX:TLABAllocationWeight=70

7. TLABRefillWasteFraction

說明: 在一次 TLAB 再填充(refill)發生的時候,最大的 TLAB 浪費。至於什麼是再填充(refill),什麼是 TLAB 浪費,會在接下來的原理說明內詳細說明

默認:64

舉例-XX:TLABRefillWasteFraction=32

8. TLABWasteIncrement

說明: TLAB 緩慢分配時容許的 TLAB 浪費增量,什麼是 TLAB 浪費,什麼是 TLAB 緩慢分配,會在接下來的原理說明內詳細說明。單位不是字節,而是MarkWord個數,也就是 Java 堆的內存最小單元

默認:4

舉例-XX:TLABWasteIncrement=4

9. ZeroTLAB

說明: 是否將新建立的 TLAB 內的對象全部字段歸零

默認:false

舉例-XX:+ZeroTLAB

TLAB 生命週期與原理詳解

TLAB 是從堆上 Eden 區的分配的一塊線程本地私有內存。線程初始化的時候,若是 JVM 啓用了 TLAB(默認是啓用的, 能夠經過 -XX:-UseTLAB 關閉),則會建立並初始化 TLAB。同時,在 GC 掃描對象發生以後,線程第一次嘗試分配對象的時候,也會建立並初始化 TLAB
在 TLAB 已經滿了或者接近於滿了的時候,TLAB 可能會被釋放回 Eden。GC 掃描對象發生時,TLAB 會被釋放回 Eden。TLAB 的生命週期指望只存在於一個 GC 掃描週期內。在 JVM 中,一個 GC 掃描週期,就是一個epoch。那麼,能夠知道,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 的空區域有關。

image

爲什麼要填充 dummy object

因爲 TLAB 僅線程內知道哪些被分配了,在 GC 掃描發生時返回 Eden 區,若是不填充的話,外部並不知道哪一部分被使用哪一部分沒有,須要作額外的檢查,若是填充已經確認會被回收的對象,也就是 dummy object, GC 會直接標記以後跳過這塊內存,增長掃描效率。反正這塊內存已經屬於 TLAB,其餘線程在下次掃描結束前是沒法使用的。這個 dummy object 就是 int 數組。爲了必定能有填充 dummy object 的空間,通常 TLAB 大小都會預留一個 dummy object 的 header 的空間,也是一個 int[] 的 header,因此 TLAB 的大小不能超過int 數組的最大大小,不然沒法用 dummy object 填滿未使用的空間。

TLAB 的大小: 若是指定了TLABSize,就用這個大小做爲初始大小。若是沒有指定,則按照以下的公式進行計算:
*`Eden 區大小 / (當前 epcoh 內會分配對象指望線程個數 每一個 epoch 內每一個線程 refill 次數配置)`**

當前 epcoh 內會分配對象指望線程個數,也就是會建立並初始化 TLAB 的線程個數,這個從以前提到的 EMA (Exponential Moving Average 指數平均數)算法採集預測而來。算法是:

採樣次數小於等於 100 時,每次採樣:
1. 次數權重 = 100 / 次數
2. 計算權重 = 次數權重 與 TLABAllocationWeight 中大的那個
3. 新的平均值 = (100% - 計算權重%) * 以前的平均值 + 計算權重% * 當前採樣值
採樣次數大於 100 時,每次採樣:
新的平均值 = (100% - TLABAllocationWeight %) * 以前的平均值 + TLABAllocationWeight % * 當前採樣值

能夠看出 TLABAllocationWeight 越大,則最近的線程數量對於這個下個 epcoh 內會分配對象指望線程個數影響越大。

每一個 epoch 內指望 refill 次數就是在每一個 GC 掃描週期內,refill 的次數。那麼什麼是 refill 呢?

在 TLAB 內存充足的時候分配對象就是快分配,不然在 TLAB 內存不足的時候分配對象就是慢分配慢分配可能會發生兩種處理:

1.線程獲取新的 TLAB。老的 TLAB 迴歸 Eden,以後線程獲取新的 TLAB 分配對象。
image
2.對象在 TLAB 外分配,也就 Eden 區。
image

這兩種處理主要由TLAB最大浪費空間決定,這是一個動態值初始TLAB最大浪費空間 = TLAB 的大小 / TLABRefillWasteFraction。根據前面提到的這個 JVM 參數,默認爲TLAB 的大小的 64 分之一。以後,伴隨着每次慢分配,這個TLAB最大浪費空間會每次遞增 TLABWasteIncrement 大小的空間。若是當前 TLAB 的剩餘容量大於TLAB最大浪費空間,就不在當前TLAB分配,直接在 Eden 區進行分配。若是剩餘容量小於TLAB最大浪費空間,就丟棄當前 TLAB 迴歸 Eden,線程獲取新的 TLAB 分配對象。refill 指的就是這種線程獲取新的 TLAB 分配對象的行爲。

那麼,也就好理解爲什麼要儘可能知足 TLAB 的大小 = Eden 區大小 / (下個 epcoh 內會分配對象指望線程個數 * 每一個 epoch 內每一個線程 refill 次數配置)了。儘可能讓全部對象在 TLAB 內分配,也就是 TLAB 可能要佔滿 Eden。在下次 GC 掃描前,refill 回 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 次。

TLABResize 設置爲 true 的時候,在每一個 epoch 當線程須要分配對象的時候, TLAB 大小都會被從新計算,並用這個最新的大小去從 Eden 申請內存。若是沒有對象分配則不從新計算,也不申請(廢話~~~)。主要是爲了能讓線程 TLAB 的 refill 次數 接近於 每一個 epoch 內每一個線程 refill 次數配置。這樣就能讓浪費比例接近於用戶配置的 TLABWasteTargetPercent.這個大小從新計算的公式爲:
TLAB 最新大小 * EMA refill 次數 / 每一個 epoch 內每一個線程 refill 次數配置

TLAB 相關源碼詳解

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;                    //每一個 epoch 週期內指望的 refill 次數

//如下是 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 佔用全部TLAB最大空間(通常是Eden大小)的指望比例,經過 EMA 算法採集預測

//如下是咱們這裏不用太關心的 field
HeapWord* _allocation_end;                    // TLAB 真正能夠用來分配內存的結束地址,這個是 _end 結束地址排除保留空間,至於爲什麼須要保留空間咱們這裏先不用關心,稍後咱們會解釋這個參數
HeapWord* _pf_top;                            // Allocation Prefetch CPU 緩存優化機制相關須要的參數,這裏先不用考慮
size_t    _allocated_before_last_gc;          // GC統計數據採集相關,例如線程內存申請數據統計等等,這裏先不用關心
unsigned  _number_of_refills;                 // 線程分配內存數據採集相關,TLAB 剩餘空間不足分配次數
unsigned  _fast_refill_waste;                 // 線程分配內存數據採集相關,TLAB 快速分配浪費,什麼是快速分配,待會會說到
unsigned  _slow_refill_waste;                 // 線程分配內存數據採集相關,TLAB 慢速分配浪費,什麼是慢速分配,待會會說到
unsigned  _gc_waste;                          // 線程分配內存數據採集相關,gc浪費
unsigned  _slow_allocations;                  // 線程分配內存數據採集相關,TLAB 慢速分配計數 
size_t    _allocated_size;                    //分配的內存大小
size_t    _bytes_since_last_sample_point;     // JVM TI 採集指標相關 field,這裏不用關心

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();
}

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(); 
}

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 的空區域有關。這個緣由以前已經說明了。

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);
}

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;
}

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;
}
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; }
3.2.2. 從新計算 TLAB 大小

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
_desired_size是何時變得呢?怎麼變得呢?

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());
}

那是何時調用 resize 的呢?通常是每次 GC 完成的時候。大部分的 GC 都是在gc_epilogue方法裏面調用,將每一個線程的 TLAB 均 resize 掉。

4. TLAB 回收

TLAB 回收就是指線程將當前的 TLAB 丟棄回 Eden 區。TLAB 回收有兩個時機:一個是以前提到的在分配對象時,剩餘 TLAB 空間不足,在 TLAB 滿可是浪費空間小於最大浪費空間的狀況下,回收當前的 TLAB 並獲取一個新的。另外一個就是在發生 GC 時,其實更準確的說是在 GC 開始掃描時。不一樣的 GC 可能實現不同,可是時機是基本同樣的,這裏以 G1 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);
  }
  //省略其餘代碼
}

爲什麼要確保堆內存是能夠解析的呢?這樣有利於更快速的掃描堆上對象。確保內存能夠解析裏面作了什麼呢?

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 其實就是將 ThreadLocalAllocBuffer 的堆內存指針 MarkWord 置爲 NULL
        thread->tlab().retire(&stats);
      } else {
        //當前若是不回收,則將 TLAB 填充 Dummy Object 利於解析
        thread->tlab().make_parsable();
      }
    }
  }

  stats.publish();
}

TLAB 主要流程總結

image

image

image

image

JFR 對於 TLAB 的監控

根據上面的原理以及源代碼分析,能夠得知 TLAB 是 Eden 區的一部分,主要用於線程本地的對象分配。在 TLAB 滿的時候分配對象內存,可能會發生兩種處理:

  1. 線程獲取新的 TLAB。老的 TLAB 迴歸 Eden,Eden進行管理,以後線程經過新的 TLAB 分配對象。
  2. 對象在 TLAB 外分配,也就 Eden 區。

對於 線程獲取新的 TLAB 這種處理,也就是 refill,按照 TLAB 設計原理,這個是常常會發生的,每一個 epoch 內可能會都會發生幾回。可是對象直接在 Eden 區分配,是咱們要避免的。JFR 對於

JFR 針對這兩種處理有不一樣的事件能夠監控。分別是jdk.ObjectAllocationOutsideTLABjdk.ObjectAllocationInNewTLABjdk.ObjectAllocationInNewTLAB對應 refill,這個通常咱們沒有監控的必要(在你沒有修改默認的 TLAB 參數的前提下),用這個測試並學習 TLAB 的意義比監控的意義更大。jdk.ObjectAllocationOutsideTLAB對應對象直接在 Eden 區分配,是咱們須要監控的。至於怎麼不影響線上性能安全的監控,怎麼查看並分析,怎麼解決,以及測試生成這兩個事件,會在下一節詳細分析。

相關文章
相關標籤/搜索