TLAB?深入了解JVM是如何用它优化内存分配的
什么是 TLAB?
TLAB(Thread Local Allocation Buffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。既然是一个内存分配区域,我们就先要搞清楚 Java 内存大概是如何分配的。
我们一般认为 Java 中 new 的对象都是在堆上分配,这个说法不够准确,应该是大部分对象在堆上的 TLAB分配,还有一部分在 栈上分配 或者是 堆上直接分配,可能 Eden 区也可能年老代。同时,对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。
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。
//静态全局变量
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 满的时候分配对象内存,可能会发生两种处理:
-
线程获取新的 TLAB。老的 TLAB 回归 Eden,Eden进行管理,之后线程通过新的 TLAB 分配对象。 -
对象在 TLAB 外分配,也就 Eden 区。
对于 线程获取新的 TLAB 这种处理,也就是 refill,按照 TLAB 设计原理,这个是经常会发生的,每个 epoch 内可能会都会发生几次。但是对象直接在 Eden 区分配,是我们要避免的。JFR 对于
JFR 针对这两种处理有不同的事件可以监控。分别是jdk.ObjectAllocationOutsideTLAB
和jdk.ObjectAllocationInNewTLAB
。jdk.ObjectAllocationInNewTLAB
对应 refill,这个一般我们没有监控的必要(在你没有修改默认的 TLAB 参数的前提下),用这个测试并学习 TLAB 的意义比监控的意义更大。jdk.ObjectAllocationOutsideTLAB
对应对象直接在 Eden 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。