通过 JFR 与日志深入探索 JVM - TLAB 原理详解

news2024/11/19 9:22:47

什么是 TLAB?

TLAB(Thread Local Allocation Buffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。既然是一个内存分配区域,我们就先要搞清楚 Java 内存大概是如何分配的。

我们一般认为 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,默认是启用的。

默认:true

举例:如果想关闭:-XX:-UseTLAB

2. ResizeTLAB

说明:TLAB 是否是自适应可变的,默认为是。

默认: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 的空区域有关。

为何要填充 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 分配对象。

2.对象在 TLAB 外分配,也就 Eden 区。

这两种处理主要由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 主要流程总结

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 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/82221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

模板模式

文章目录思考模板模式1.模板模式的本质2.何时选用模板模式3.优缺点4.模板方法的结构5.实现思考模板模式 模板模式其实就是抽离共用方法到抽象类中,然后再规定其具体实现步骤 1.模板模式的本质 模板方法模式的本质:固定算法骨架。 模板方法模式主要是通过制定模板&am…

系统集成企业需具备哪些证书?

IT信息化企业,系统集成企业需要做的资质证书有哪些?经常遇到有新成立的系统集成商问智达鑫业小编,该申请哪些企业资质,接下来了小编整理下目前市场上使用频率比较高的一些资质证书,大家可以参考下。 信息系统建设和服务…

A-Level考试常见问题综合解答

关于A Level的Q&A 问:参加A Level的考试与其他考试相比有什么优势吗? 答:A Level考试的门数相较其他国际课程更少,学生有更多的时间花费在每门课上取得更好的GPA和最终成绩。问:就读的学校就直接提供A Level课程&a…

jmeter断言

jmeter断言常用的有响应断言和json断言; 常用的响应断言: 1.字符串:如果响应中包含了指定的字符串,判断为成功,不支持正则表达式;如下图: 2.包括:如果响应中包含了指定的字符串&…

mac清空废纸篓怎么恢复?

众所周知,电脑只要在运行都会产生一些临时文件或者文档,而这些文件会存放在电脑的存储空间里,方便我们后续的使用。当Mac中存储的文件过多时,就会影响到我们的正常使用,只有通过清理电脑文件,来释放更多的存…

【JavaWeb开发-Servlet】拾起海中的漂流瓶超强版

目录 原版: 一、思路: 二、实现: 三、资源分享 四、部署服务器时记得修改文件路径 原版: 【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版_代码骑士的博客-CSDN博客【代码】【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版…

SMART PLC运动超驰功能编程应用(含V2.7版本固件下载)

什么是运动控制超驰功能,运动超驰功能如何开启,请参看下面的导图部分: 下面一步步教大家如何更新CPU固件版本。 S7-200 SMART PLC自定义脉冲控制功能块相关详细组态设置,请参看下面的博客。链接如下: S7-200 SMART PLC自定义脉冲轴控功能块AxisControl_FB(梯形图)_RXX…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:MapView MaptrimView

本文简述如何在Smobiler中使用MapView和MaptrimView。 Mapview MapView 地图插件,可用于显示指定地点地图,显示轨迹等。 Step 1. 新建一个SmobilerForm窗体,再拖入MapView和Button,MapView.Size设置(300,300&#xf…

Spring Batch 批处理入门案例解析

引言 书接上篇 Spring Batch 批处理入门案例 ,上篇带小伙伴们写了一个Spring Batch 入门案例,里面有哪些注意要点呢?本篇一起来分析分析~ 案例解析 整个入门案例核心点有5个,一一来讲解一下 EnableBatchProcessing 批处理启动…

AD8226组成的高精度放大电路之一

工业设备中常常需要用到高速、高精度的模拟前端方案,而其中控制系统中的信号电平通常为以下几类之一:单端电流(4 mA 至 20 mA)、单端差分电压(0 V 至 5V、0 V 至10 V、5 V、10 V)或者来自热电偶或称重传感器等传感器的小信号输入。大共模电压摆幅也非常典型,尤其是小信号…

Spring Cloud Zuul过滤器介绍及使用(传递数据、拦截请求和异常处理)

在教程《Zuul网关的介绍及使用》中一开始就介绍过,Zuul 可以实现很多高级的功能,比如限流、认证等。想要实现这些功能,必须要基于 Zuul 给我们提供的核心组件“过滤器”。下面我们一起来了解一下 Zuul 的过滤器。 过滤器类型 Zuul 中的过滤…

问题来了,拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

今天,聊一个有趣的问题:拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗? 可能有的同学会说,网线都被拔掉了,那说明物理层被断开了,那在上层的传输层理应也会断开,所以原…

MarkDown 项目中如何引入开源MarkDown? 史上最简单教程

目录 一、少不了的东西 editor.md ① 下载链接 ② 将其引入到自己的项目中 引入依赖 二、代码部分 一些小细节 1. 编辑页 2. 展示页 一、少不了的东西 如果想要在一个页面中使用MarkDown ,那么你首先就要引入MarkDown editor.md ① 下载链接 GitHub下…

Flutter和Rust如何优雅的交互

前言 文章的图片链接都是在github上,可能需要...你懂得;本文含有大量关键步骤配置图片,强烈建议在合适环境下阅读 Flutter直接调用C层还是蛮有魅力,想想你练习C,然后直接能用flutter在上层展示出效果,是不…

【中级ECharts技术】transform进行数据转换和dataZoom在项目中的使用(可视化非常的强劲)

transform 进行数据转换 数据转换是这样一个公式:outData=f(inputData)。F是转换方法,例如filter、sort、region、boxplot、cluster、aggregate(todo)等。有了数据转换功能,我们至少可以做到以下几点: 将数据分成多个部分,并在不同的饼图中显示它们。 执行一些数据统计…

C++ 注释

📒博客主页: ​​开心档博客主页​​ 🎉欢迎关注🔎点赞👍收藏⭐留言📝 📌本文由开心档原创! 📆51CTO首发时间:🌴2022年12月12日🌴 ✉…

Httpd服务进阶知识-HTTP协议详解

一.WEB开发概述 1>.C/S编程 CS即客户端、服务器编程。 客户端、服务端之间需要使用Socket,约定协议、版本(往往使用的协议是TCP或者UDP),指定地址和端口,就可以通信了。客户端、服务端传输数据,数据可以有一定的格式&#xff…

Go开发中配置一个Logger日志的功能实现(结合zap日志库)

为什么需要Logger 一般在开发项目的时候我们都是需要一个存储日志的文件,因为在部署项目以后,我们只能通过去筛查日志进行检索问题,这时候日志是否可以呈现清晰这个对于我们进行排查工作是十分重要的,所以Logger能否展示出我们最…

基于PHP的中华诗歌网的设计与实现

目 录 Abstract 2 目 录 3 1 绪论 5 1.1 研究背景 5 1.2诗歌鉴赏网站的意义 5 1.3网站开发的设计思想 5 2 系统相关技术 7 2.1 MySQL数据库介绍 7 2.2 PHP技术介绍 8 3 系统需求分析 10 3.1 系统需求分析 10 3.2系统可行性分析 10 3.3 系统用例分析 11 4 系统的详细设计 12 4.1…

QT 短时间大量图片传输,实现监控效果 (实时视屏传输) (暴力模式)

1.首先需要知道的知识 1.我使用的是 TCP 协议传输 ,因为传输数据准确一点, 2. 然后是 套接字的 信号 , readyRead( ) ,这个信号的功能是只要套接字里面的 缓存区有数据,他就会发出这个信号。 3.我们传输数据有时候,会出现 粘…