ThreadLocalAllocBuffer原理剖析前言ThreadLocalAllocBuffer原理剖析TLAB (ThreadLocalAllocBuffer) 核心设计原理1. 核心设计思想2. TLAB 的内存结构指针OpenJDK 8源码深度剖析1. TLAB 数据结构定义2. 快速路径分配Fast Path3. 慢速路径分配与 TLAB 刷新策略Slow Path4. TLAB 的“退役”与堆可解析性TLAB 核心参数动态自适应调整机制1. 期望大小 _desired_size 的计算2. 浪费阈值 _refill_waste_limit 的动态递增系统工程师视角下的 TLAB 调优总结前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正ThreadLocalAllocBuffer原理剖析TLAB (ThreadLocalAllocBuffer) 核心设计原理在多线程并发的高并发应用场景下虚拟机堆内存的年轻代Eden 区是所有线程共享的。如果多个线程同时申请分配内存传统的分配方式必须通过加锁或者采用CAS (Compare And Swap)自旋操作来保证指针更新的原子性。在高并发下这种对单一指针Top 指针的竞争会成为严重的性能瓶颈。为了解决这一问题OpenJDK 引入了TLAB (ThreadLocalAllocBuffer)技术。1. 核心设计思想内存独占化从共享的 Eden 区域中为每个线程预先分配一块专属的内存区域即 TLAB。无锁化分配Fast Path当线程内部需要创建对象时优先在自己的 TLAB 中进行分配。由于这块内存在同一时刻只属于该线程分配逻辑只需要移动指针Pointer Bumping即可没有任何并发锁竞争性能极高。全局同步退化Slow Path只有当线程的 TLAB 空间耗尽需要向 Eden 申请一块新的 TLAB或者对象体积过大无法在 TLAB 中容纳时才会触发全局同步机制使用 CAS 抢占 Eden 空间。2. TLAB 的内存结构指针一个 TLAB 区域的核心由四个关键指针来界定start指向当前 TLAB 内存块的起始首地址。top指向当前 TLAB 内部已分配内存与未分配内存的分界点。每次成功分配对象top指针向前推进对象大小。end指向当前 TLAB 的逻辑终点通常end hard_end - alignment_reserve保留对齐空间。hard_end指向当前 TLAB 的物理真实边界等于申请到的内存块末尾。OpenJDK 8源码深度剖析在 OpenJDK 8源码中TLAB 的数据结构定义在src/share/vm/gc_implementation/shared/threadLocalAllocBuffer.hpp而具体的分配退化逻辑和补充策略则在对应的.cpp文件及CollectedHeap中。1. TLAB 数据结构定义以下是ThreadLocalAllocBuffer类的核心成员变量与解析// 源码路径src/share/vm/gc_implementation/shared/threadLocalAllocBuffer.hppclassThreadLocalAllocBuffer:publicCHeapObjmtThread{friendclassVMStructs;private:HeapWord*_start;// TLAB 内存区域的起始地址HeapWord*_top;// 当前分配指针指向下一个空闲位置HeapWord*_end;// 逻辑终点预留了对象的对齐填充空间HeapWord*_hard_end;// 物理终点从 Eden 申请到的真实末尾位置size_t _desired_size;// 期望的 TLAB 大小单位为 HeapWords根据运行期动态计算size_t _refill_waste_limit;// 拒绝分配并引发 refill 的最大浪费空间阈值staticsize_t _target_refill_waste_allocations;// 在一次 GC 周期内期望每个线程 refill 的目标次数// 默认值由参数 -XX:TLABWasteTargetPercent 控制默认 1%// 统计指标用于动态调整 TLAB 大小size_t _number_of_refills;// 当前线程 TLAB 重新填充的次数size_t _fast_alloc_attempts;// 快速分配Fast Path尝试次数size_t _slow_alloc_attempts;// 慢速分配Slow Path尝试次数size_t _gc_waste;// 发生 GC 时由于未用完而被浪费的内存总量// ... 忽略部分辅助方法public:// 初始化及置空逻辑voidinitialize(HeapWord*start,HeapWord*top,HeapWord*end);voidclear();// 核心分配方法Fast Path 指针碰撞inlineHeapWord*allocate(size_t size);};2. 快速路径分配Fast Path当我们在 Java 层通过new关键字创建对象或者是字节码解释器/JIT 编译器遇到分配指令时会直接内联执行快速分配。其本质就是无锁的指针叠加。// 源码路径src/share/vm/gc_implementation/shared/threadLocalAllocBuffer.inline.hppinlineHeapWord*ThreadLocalAllocBuffer::allocate(size_t size){// in_use() 检查当前 TLAB 是否已经初始化并激活if(gclog_or_tty!NULLGC_egress_bits_words0){// 生产环境中主要直接走下面的无锁指针碰撞}HeapWord*objtop();// 核心判断如果当前 top 指针加上所需大小小于等于逻辑终点 endif(pointer_delta(end(),obj)size){// 成功更新 top 指针并直接返回原 top 地址即对象首地址set_top(objsize);returnobj;}// 空间不足返回 NULL意味着需要进入慢速分配路径Slow PathreturnNULL;}3. 慢速路径分配与 TLAB 刷新策略Slow Path当 Fast Path 返回NULL时JVM 运行时会调用CollectedHeap::allocate_from_tlab_slow方法。在这个阶段JVM 需要做出一个关键决策是放弃当前 TLAB 申请个新的还是保留当前 TLAB让大对象直接分配在堆Eden中// 源码路径src/share/vm/gc_interface/collectedHeap.inline.hppHeapWord*CollectedHeap::allocate_from_tlab_slow(Thread*thread,size_t size){// 1. 尝试在当前线程现有的 TLAB 剩余空间里进行“慢速分配”// 这种情况通常是因为并发控制或某些特殊的对齐要求导致的HeapWord*objthread-tlab().allocate(size);if(obj!NULL){returnobj;}// 2. 走到这里说明 TLAB 确实没有足够的空间容纳当前 size 的对象// 检查当前 TLAB 的剩余空间浪费空间是否超过了 _refill_waste_limit 阈值// 核心公式剩余空间 end - topsize_t free_wordspointer_delta(thread-tlab().end(),thread-tlab().top());if(free_wordsthread-tlab().refill_waste_limit()){// 场景 A剩余空间小于阈值说明浪费得起// 记入统计指标这部分未分配空间将作为 GC 浪费处理thread-tlab().record_gc_waste(free_words);// 废弃旧的 TLAB为了维持堆的连续性和可解析性Parsability// 必须把旧 TLAB 的剩余空间用一个“填充物对象”通常是 int 数组填满thread-tlab().retire();}else{// 场景 B剩余空间大于等于阈值说明里面还有很多空闲内存丢弃它太可惜了// 此时选择保留当前的 TLAB不进行刷新。// 转而直接在共享的 Eden 区通过 CAS 竞争分配当前的大对象Direct Allocationreturnallocate_outside_tlab(size,thread);}// 3. 申请分配并初始化一块全新的 TLAB// 首先计算新 TLAB 的期望大小size_t new_tlab_sizethread-tlab().compute_size(size);// 强制使旧 TLAB 失效将其指针重置thread-tlab().clear();if(new_tlab_size0){returnNULL;}// 4. 从 Eden 区中通过全局 CAS 锁申请一块新的大内存块// 此处调用具体垃圾回收器的内存分配如 G1, ParallelGC 等HeapWord*actual_tlab_startallocate_new_tlab(new_tlab_size);if(actual_tlab_startNULL){returnNULL;// 内存不足触发 GC}// 计算真实的物理边界和逻辑边界HeapWord*actual_tlab_endactual_tlab_startnew_tlab_size;// 5. 将这块全新的内存绑定给当前线程重新初始化该线程的 TLAB 指针thread-tlab().initialize(actual_tlab_start,actual_tlab_startsize,actual_tlab_end);// 新 TLAB 已经扣除了当前对象所需的大小通过上面初始化时将 top 设为 start size// 直接返回这块新内存的起始地址作为对象的首地址returnactual_tlab_start;}4. TLAB 的“退役”与堆可解析性当旧的 TLAB 被放弃或者发生 GC 时由于 TLAB 的_top指针可能没有走到_end这部分空白区域在堆中必须能够被垃圾回收器正确识别。垃圾回收器通过顺序扫描堆来标记对象如果遇到无规则的“乱码”内存会导致崩溃。因此JVM 引入了堆的可解析性Parsability在丢弃 TLAB 前必须在[top, end)这段空白区域填充一个虚设的、合法的结构通常是一个int[]类型的 Dummy 填充对象。// src/share/vm/memory/threadLocalAllocBuffer.cppvoidThreadLocalAllocBuffer::clear_before_allocation(){_slow_refill_wastefree();// 统计被浪费的内存空间// 核心将当前 TLAB 剩余的空白区包装为 Dummy 对象保持堆全局可连续扫描make_parsable(true);// 重置当前线程的 TLAB 指针为 NULL_start_top_endNULL;}voidThreadLocalAllocBuffer::make_parsable(boolretire){if(CMSIncrementalMode||!ParsableTLAB)return;// 如果包含了有效的内存区间if(start()!NULL){assert(top()!NULLend()!NULL,inconsistency);if(retire){// 如果确定要退休将一些统计数据落地}// 在当前 top 到 end 之间注入一个 int 数组Filler Object// 这样 GC 扫描到这里时会认为这是一个普通的 int 数组对象从而可以直接跳过这段空白区CollectedHeap::fill_with_object(top(),end(),retire);// 写入 Dummy 对象后逻辑上将 _top 推进到 _end表示该 TLAB 已经完全填满set_top(end());}}TLAB 核心参数动态自适应调整机制HotSpot 默认开启了-XX:ResizeTLAB。这意味着 TLAB 的大小_desired_size以及浪费阈值_refill_waste_limit并不是固定不变的而是随着应用的运行、线程的分配速率动态计算的。1. 期望大小_desired_size的计算在每个线程发生 Refill 或者 GC 触发 TLAB 重置时JVM 会根据当前线程近期的内存分配行为计算下一次所需的 TLAB 大小// src/share/vm/memory/threadLocalAllocBuffer.cppsize_tThreadLocalAllocBuffer::compute_size(size_t obj_size){// 根据历史分配行为、线程总数以及 Eden 区总大小估算一个期望的字长Wordssize_t blk_sizealloc_fraction()*TargetPLABWastePct;// 确保新计算出来的 TLAB 大小能够容纳当前请求分配的对象 obj_sizesize_t min_sizealign_object_size(obj_sizealignment_reserve());size_t sizeMAX2(blk_size,min_size);// 限制不能超过 TLAB 的最大上限通常是 Eden 的一个比例或固定的最大值sizeMIN2(size,max_size());returnsize;}在每一个 GC 周期结束时JVM 会根据线程过去分配的内存速率重新计算_desired_sizenew_desired_size Thread Allocation Rate Target Refill Waste Allocations \text{new\_desired\_size} \frac{\text{Thread Allocation Rate}}{\text{Target Refill Waste Allocations}}new_desired_sizeTarget Refill Waste AllocationsThread Allocation Rate​如果线程在两个 GC 周期内频繁申请内存_desired_size会逐渐调大从而减少由于 TLAB 耗尽而进入 Slow Path 全局加锁的次数。2. 浪费阈值_refill_waste_limit的动态递增为了防止线程频繁触发慢速分配直接去公共 Eden 分配对象而不 refill如果线程频繁在共享空间分配大对象JVM 会自适应地拉高_refill_waste_limit。为了防止空间浪费_refill_waste_limit也是动态递增的。当线程不断触发 TLAB 刷新且每次都留下大块空白时JVM 会调高该线程的_refill_waste_limit。这意味着阈值变大后更不容易满足free_words refill_waste_limit的条件从而迫使大对象直接去 Eden 区分配保护了 TLAB 的空间不被频繁废弃。voidThreadLocalAllocBuffer::record_slow_allocation(size_t word_size){// 每触发一次慢速分配说明当前的配置可能导致了较多的共享空间竞争_slow_allocations;// 动态调大浪费阈值增加 TLABRefillWasteIncrement默认值为 4// 阈值变大意味着后续即使 TLAB 剩余空间较多也允许丢弃并 refill从而减少进入共享空间竞争的次数_refill_waste_limitTLABRefillWasteIncrement;}系统工程师视角下的 TLAB 调优总结理解了上述源码实现在遇到高并发、高吞吐量的 Java 系统性能瓶颈时可以采取以下生产级调优策略参数默认值系统工程师调优建议-XX:UseTLABtrue高并发系统严禁关闭此参数。-XX:ResizeTLABtrue默认开启。若系统运行中对象分配速率极度平稳可考虑关闭以减少运行期计算开销若波动大保持开启。-XX:TLABSize设置值0(动态)默认由 JVM 自动计算。如果在性能监控如 JFR中发现大量的object_allocation_outside_tlab事件且对象并非超大对象可显式调大此参数如512k或1m。-XX:TLABWasteTargetPercentN1(%)每一个 TLAB 占 Eden 区的百分比。高并发、多线程环境下如果线程数极多可适当调小该值如改为1或更小防止 TLAB 占用过多 Eden 空间导致频繁的 Young GC。关键认知TLAB 的本质是一种空间换时间及解耦并发竞争的思想。它通过赋予线程局部独占性将对象的快速分配拉高到了极致。但代价是会产生一定的堆内存碎片即被 Filler 对象填充的 Gap 空间。作为系统工程师调优的核心平衡点就在于“全局 CAS 竞争的锁开销”与“碎片化导致的 Young GC 频率变高”之间寻找最优解。