全系列目录:经过 JFR 与日志深刻探索 JVM - 总览篇git
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 参数以及其含义,在下一小节会深刻源码分析原理以及设计这个参数是为什么。缓存
如下参数与默认值均来自于 OpenJDK 11安全
UseTLAB
说明:是否启用 TLAB,默认是启用的。markdown
默认:true多线程
举例:若是想关闭:-XX:-UseTLAB
ide
ResizeTLAB
说明:TLAB 是不是自适应可变的,默认为是。函数
默认:true
举例:若是想关闭:-XX:-ResizeTLAB
说明:初始 TLAB 大小。单位是字节
默认:0, 0 就是不主动设置 TLAB 初始大小,而是经过 JVM 本身计算每个线程的初始大小
举例:-XX:TLABSize=65536
说明:最小 TLAB 大小。单位是字节
默认:2048
举例:-XX:TLABSize=4096
说明:TLAB 的大小计算涉及到了 Eden 区的大小以及能够浪费的比率。TLAB 浪费占用 Eden 的百分比,这个参数的做用会在接下来的原理说明内详细说明
默认:1
举例:-XX:TLABWasteTargetPercent=10
说明: TLAB 大小计算和线程数量有关,可是线程是动态建立销毁的。因此须要基于历史线程个数推测接下来的线程个数来计算 TLAB 大小。通常 JVM 内像这种预测函数都采用了 EMA (Exponential Moving Average 指数平均数)算法进行预测,会在接下来的原理说明内详细说明。这个参数表明权重,权重越高,最近的数据占比影响越大。
默认:35
举例:-XX:TLABAllocationWeight=70
说明: 在一次 TLAB 再填充(refill)发生的时候,最大的 TLAB 浪费。至于什么是再填充(refill),什么是 TLAB 浪费,会在接下来的原理说明内详细说明
默认:64
举例:-XX:TLABRefillWasteFraction=32
说明: TLAB 缓慢分配时容许的 TLAB 浪费增量,什么是 TLAB 浪费,什么是 TLAB 缓慢分配,会在接下来的原理说明内详细说明。单位不是字节,而是MarkWord
个数,也就是 Java 堆的内存最小单元
默认:4
举例:-XX:TLABWasteIncrement=4
说明: 是否将新建立的 TLAB 内的对象全部字段归零
默认:false
举例:-XX:+ZeroTLAB
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 次数配置
。
线程初始化的时候,若是 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,这里不用关心
首先是 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(); }
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(); }
不一样的 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 的空区域有关。这个缘由以前已经说明了。
当 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); }
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; }
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; }
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; }
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
掉。
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 是 Eden 区的一部分,主要用于线程本地的对象分配。在 TLAB 满的时候分配对象内存,可能会发生两种处理:
对于 线程获取新的 TLAB 这种处理,也就是 refill,按照 TLAB 设计原理,这个是常常会发生的,每一个 epoch 内可能会都会发生几回。可是对象直接在 Eden 区分配,是咱们要避免的。JFR 对于
JFR 针对这两种处理有不一样的事件能够监控。分别是jdk.ObjectAllocationOutsideTLAB
和jdk.ObjectAllocationInNewTLAB
。jdk.ObjectAllocationInNewTLAB
对应 refill,这个通常咱们没有监控的必要(在你没有修改默认的 TLAB 参数的前提下),用这个测试并学习 TLAB 的意义比监控的意义更大。jdk.ObjectAllocationOutsideTLAB
对应对象直接在 Eden 区分配,是咱们须要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。