news 2026/6/21 10:11:22

JVM内存分配管理ThreadLocalAllocBuffer原理剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM内存分配管理ThreadLocalAllocBuffer原理剖析

ThreadLocalAllocBuffer原理剖析

  • 前言
  • ThreadLocalAllocBuffer原理剖析
    • TLAB (ThreadLocalAllocBuffer) 核心设计原理
      • 1. 核心设计思想
      • 2. TLAB 的内存结构指针
    • OpenJDK 8源码深度剖析
      • 1. TLAB 数据结构定义
      • 2. 快速路径分配(Fast Path)
      • 3. 慢速路径分配与 TLAB 刷新策略(Slow Path)
      • 4. 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:publicCHeapObj<mtThread>{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!=NULL&&GC_egress_bits_words>0){// 生产环境中主要直接走下面的无锁指针碰撞}HeapWord*obj=top();// 核心判断:如果当前 top 指针加上所需大小小于等于逻辑终点 endif(pointer_delta(end(),obj)>=size){// 成功:更新 top 指针,并直接返回原 top 地址(即对象首地址)set_top(obj+size);returnobj;}// 空间不足,返回 NULL,意味着需要进入慢速分配路径(Slow Path)returnNULL;}

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*obj=thread->tlab().allocate(size);if(obj!=NULL){returnobj;}// 2. 走到这里说明 TLAB 确实没有足够的空间容纳当前 size 的对象// 检查当前 TLAB 的剩余空间(浪费空间)是否超过了 _refill_waste_limit 阈值// 核心公式:剩余空间 = end - topsize_t free_words=pointer_delta(thread->tlab().end(),thread->tlab().top());if(free_words<thread->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 Allocation)returnallocate_outside_tlab(size,thread);}// 3. 申请分配并初始化一块全新的 TLAB// 首先计算新 TLAB 的期望大小size_t new_tlab_size=thread->tlab().compute_size(size);// 强制使旧 TLAB 失效,将其指针重置thread->tlab().clear();if(new_tlab_size==0){returnNULL;}// 4. 从 Eden 区中通过全局 CAS 锁申请一块新的大内存块// 此处调用具体垃圾回收器的内存分配(如 G1, ParallelGC 等)HeapWord*actual_tlab_start=allocate_new_tlab(new_tlab_size);if(actual_tlab_start==NULL){returnNULL;// 内存不足,触发 GC}// 计算真实的物理边界和逻辑边界HeapWord*actual_tlab_end=actual_tlab_start+new_tlab_size;// 5. 将这块全新的内存绑定给当前线程,重新初始化该线程的 TLAB 指针thread->tlab().initialize(actual_tlab_start,actual_tlab_start+size,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_waste+=free();// 统计被浪费的内存空间// 核心:将当前 TLAB 剩余的空白区包装为 Dummy 对象,保持堆全局可连续扫描make_parsable(true);// 重置当前线程的 TLAB 指针为 NULL_start=_top=_end=NULL;}voidThreadLocalAllocBuffer::make_parsable(boolretire){if(CMSIncrementalMode||!ParsableTLAB)return;// 如果包含了有效的内存区间if(start()!=NULL){assert(top()!=NULL&&end()!=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 区总大小,估算一个期望的字长(Words)size_t blk_size=alloc_fraction()*TargetPLABWastePct;// 确保新计算出来的 TLAB 大小能够容纳当前请求分配的对象 obj_sizesize_t min_size=align_object_size(obj_size+alignment_reserve());size_t size=MAX2(blk_size,min_size);// 限制不能超过 TLAB 的最大上限(通常是 Eden 的一个比例或固定的最大值)size=MIN2(size,max_size());returnsize;}

在每一个 GC 周期结束时,JVM 会根据线程过去分配的内存速率,重新计算_desired_size

new_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_size=Target 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_limit+=TLABRefillWasteIncrement;}

系统工程师视角下的 TLAB 调优总结

理解了上述源码实现,在遇到高并发、高吞吐量的 Java 系统性能瓶颈时,可以采取以下生产级调优策略:

参数默认值系统工程师调优建议
-XX:+UseTLABtrue高并发系统严禁关闭此参数。
-XX:+ResizeTLABtrue默认开启。若系统运行中对象分配速率极度平稳,可考虑关闭以减少运行期计算开销;若波动大,保持开启。
-XX:TLABSize=设置值0(动态)默认由 JVM 自动计算。如果在性能监控(如 JFR)中发现大量的object_allocation_outside_tlab事件,且对象并非超大对象,可显式调大此参数(如512k1m)。
-XX:TLABWasteTargetPercent=N1(%)每一个 TLAB 占 Eden 区的百分比。高并发、多线程环境下,如果线程数极多,可适当调小该值(如改为1或更小),防止 TLAB 占用过多 Eden 空间导致频繁的 Young GC。

关键认知:TLAB 的本质是一种空间换时间解耦并发竞争的思想。它通过赋予线程局部独占性,将对象的快速分配拉高到了极致。但代价是会产生一定的堆内存碎片(即被 Filler 对象填充的 Gap 空间)。作为系统工程师,调优的核心平衡点就在于“全局 CAS 竞争的锁开销”“碎片化导致的 Young GC 频率变高”之间寻找最优解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/21 10:06:28

自动化测试工程师(含测试开发SDET)全景解析:薪资待遇·发展前景·岗位分级·职业路径(2026实战版)

基于2025—2026年BOSS直聘、猎聘、职友集、51Testing及行业薪资报告整理。文中"自动化测试工程师"含接口/UI自动化测试岗&#xff0c;"测试开发/SDET"指具备开发能力、能设计测试框架与质量平台的岗位。两者薪资与要求有明显梯度。一、岗位定义与行业现状1…

作者头像 李华
网站建设 2026/6/21 9:57:13

B站会员购抢票攻略:如何用Python工具优雅应对秒杀挑战?

B站会员购抢票攻略&#xff1a;如何用Python工具优雅应对秒杀挑战&#xff1f; 【免费下载链接】biliTickerBuy b站会员购购票辅助工具 项目地址: https://gitcode.com/GitHub_Trending/bi/biliTickerBuy 当B站热门活动门票开售&#xff0c;你是否经历过这样的场景&…

作者头像 李华
网站建设 2026/6/21 9:57:02

英雄联盟战绩查询终极指南:3步安全使用Seraphine保护你的账号

英雄联盟战绩查询终极指南&#xff1a;3步安全使用Seraphine保护你的账号 【免费下载链接】Seraphine 英雄联盟战绩查询工具 项目地址: https://gitcode.com/gh_mirrors/se/Seraphine 作为英雄联盟玩家&#xff0c;你是否经常想查看自己的战绩数据&#xff0c;但又担心第…

作者头像 李华
网站建设 2026/6/21 9:53:26

端侧流式语音识别实战:Nemotron模型与ONNX Runtime的部署优化

1. 项目概述&#xff1a;当流式语音识别遇上端侧部署最近在折腾一个老项目&#xff0c;需要把语音识别&#xff08;ASR&#xff09;能力塞进一个资源相当有限的嵌入式设备里。需求很明确&#xff1a;要实时、要流式&#xff08;不能等用户说完一整句再识别&#xff09;、要离线…

作者头像 李华
网站建设 2026/6/21 9:51:35

嵌入式开发利器:飞思卡尔i.MX系列ATK工具烧录与镜像转换实战指南

1. 项目概述与工具定位在嵌入式开发这条路上&#xff0c;无论你是做消费电子、工业控制还是物联网设备&#xff0c;从写好代码到让代码在硬件上跑起来&#xff0c;中间总有两道绕不开的坎&#xff1a;一是怎么把编译好的程序&#xff08;镜像&#xff09;烧录到板子的Flash存储…

作者头像 李华
网站建设 2026/6/21 9:37:53

Lion优化器:极简设计如何影响泛化与收敛性?

1. 从“狮子”到“猎手”&#xff1a;Lion优化器的核心吸引力与待解之谜去年初&#xff0c;一篇名为《Symbolic Discovery of Optimization Algorithms》的论文在机器学习社区扔下了一颗重磅炸弹。它提出的Lion优化器&#xff0c;以其简洁到令人惊讶的公式和声称媲美甚至超越Ad…

作者头像 李华