全系列目录:通过 JFR 与日志深入探索 JVM - 总览篇

什么是 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 COMPILER2if (is_server_compilation_mode_vm()) {int lines =  MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;_reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /(int)HeapWordSize;}
#endif// 初始化 main 线程的 TLABguarantee(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 fieldsthis->initialize_tlab();//剩余代码忽略
}

分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。
src/hotspot/share/runtime/thread.hpp

void initialize_tlab() {//如果没有通过 -XX:-UseTLAB 禁用 TLAB,则初始化TLABif (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 分配内存,所以这里都设置为 NULLinitialize(NULL,                    // startNULL,                    // topNULL);                   // 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) / TLABRefillWasteFractionset_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 / HeapWordSizeif (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 GCint 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 空间为当前线程的 TLABtlab.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,也就是 TLABWasteIncrementset_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);//如果全局启用了 TLABif (UseTLAB) {//如果指定要回收,则回收 TLABif (retire_tlabs) {//回收 TLAB 其实就是将 ThreadLocalAllocBuffer 的堆内存指针 MarkWord 置为 NULLthread->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 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。

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

  1. 通过 JFR 与日志深入探索 JVM - TLAB JFR 相关事件与日志详解

    全系列目录:通过 JFR 与日志深入探索 JVM - 总览篇 上一篇我们详细的分析了 TLAB 的原理以及生命周期,并且提出 JFR 相关的两个事件:在线程分配对象时,如果 TLAB 不够,则根据最大 ...

  2. 为什么jvm要分为堆、方法区等?原理是什么?_「JVM」知识点详解一:JVM运行原理详解...

    前言 JVM 一直都是面试的必考点,大家都知道,但是要把它搞清楚又好像不是特别容易.JVM 的知识点太散,不系统,今天带大家详细的了解一下jvm的运行原理. 正文 1 什么是JVM? JVM是Java ...

  3. 通过 JFR 与日志深入探索 JVM - 调试 JVM 的工具 WhiteBox API

    在之后的 JFR 事件学习以及调试的过程中,我们会经常用到 WhiteBox API 来触发 JVM 的一些机制或者临界点.例如强制 JVM 现在立刻进行 FullGC 等等. 什么是 WhiteBo ...

  4. JVM 启动参数详解

    JVM 启动参数详解 JVM 作为一个通用的虚拟机,我们可以通过启动Java命令时指定不同JVM参数,让 JVM调整自己的运行状态和行为,内存管理和垃圾回收的GC算法等等. 直接通过命令行启动 Jav ...

  5. 【JAVA进阶】JVM第二篇- JVM 垃圾回收详解

    写在前面的话 脑子是个好东西,可惜的是一直没有搞懂脑子的内存删除机制是什么,所以啊,入行多年,零零散散的文章看了无数,却总是学习了很多也忘了很多. 痛定思痛的我决定从今天开始系统的梳理下知识架构,记录 ...

  6. JDK自带JVM分析工具详解

    JDK自带JVM分析工具详解 1. JVM分析工具概述 1.1 JVM分析工具简介 1.2 JVM分析工具分类 2. JVM分析工具详解 2.1 idea环境配置 2.2 jps 2.3 jinfo ...

  7. java解析日志数据_Java实时监控日志文件并输出的方法详解

    Java实时监控日志文件并输出的方法详解 想在前台显示数据同步过程中产生的日志文件,在网上找到解决方案,做了代码测试好用.这里做个记录 java.io.RandomAccessFile可以解决同时向文 ...

  8. [java] 虚拟机(JVM)底层结构详解[转]

    [java] 虚拟机(JVM)底层结构详解[转] 本文来自:曹胜欢博客专栏.转载请注明出处:http://blog.csdn.net/csh624366188 在以前的博客里面,我们介绍了在java领 ...

  9. JVM垃圾回收算法与原理详解

    垃圾回收 参考文档 GC参考手册-Java版 理解Java的强引用.软引用.弱引用和虚引用 JVM系列(五) - JVM垃圾回收算法 如何判断对象可以回收 引用计数法 参考文章 Java JVM的引用 ...

最新文章

  1. C++ 学习路线推荐
  2. 云炬WEB开发笔记 2-3git详细安装教程及下载太慢的解决办法
  3. VTK:Utilities之BrownianPoints
  4. 大学生python实验心得体会_大学生实验心得体会精选例文【三篇】
  5. 作者:钱卫宁(1976-),男,华东师范大学数据科学与工程研究院教授、博士生导师...
  6. 数据管理流程,基础入门简介
  7. 《进化——我们在互联网上奋斗的故事》一一1.9 职业素养中的品德细节
  8. 数字电视 机顶盒原理
  9. 吉安市推行“区块链+电子证照+无证办理”模式
  10. 不要老盯着存储,存储的价值在于数据流:Filenet
  11. jquery div点击展开,点击收起,点击除了该div外任何地方,该div隐藏
  12. 捋一捋Python中的List(下)
  13. 《活出生命的意义》阅读笔记
  14. MUD教程--巫师入门教程3
  15. 直播回放:快速上手,使用 Kotlin 把支付宝小程序装进自己的 App
  16. 智能语音将成下一代人机交互新入口
  17. 【OCR】文字检测:传统算法、CTPN、EAST
  18. code representation-CPG
  19. Mesh与WiFi的区别
  20. 用于太阳能水蒸发的分层氮化钛纳米管网的制备和光热转化特性

热门文章

  1. 从零开始实现微信小程序上线发布流程
  2. 快应用官网 | 2020年端午假期工作安排
  3. UVA 10635 Prince and Princess
  4. 查看期刊最近的影响因子
  5. 基于Java毕业设计东理咨询交流论坛源码+系统+mysql+lw文档+部署软件
  6. Golang lua交互——gopher-lua中call函数使用
  7. 通过站点优化记录规划书
  8. Scrum板与Kanban如何抉择?敏捷工具:czsva板与按照mgzaqbpe
  9. 微信小程序【生命周期】
  10. python调用函数实现银行ATM典型案例练习