← 返回题目列表

什么是 TLAB?它是怎么解决多线程对象分配的竞争问题的?

困难 第 26 / 34 题 更新于 2026/07/28
TLAB对象分配并发Eden

简化版

TLAB(Thread Local Allocation Buffer,线程本地分配缓冲区)是 JVM 为「加速多线程对象分配」的一个优化:给每个线程在 Eden 区里预先划一小块「私有」的内存,线程分配对象时优先在自己的这块 TLAB 里分配,不用和别的线程抢要解决的问题:对象都分配在 Eden 区(共享),多个线程同时分配就要竞争——如果每次分配都用 CAS/加锁去抢 Eden 的分配指针,高并发下竞争激烈、慢。TLAB 的思路:每个线程一次性从 Eden「批发」一块(如 100KB)作为私有缓冲区,之后在自己的 TLAB 里分配对象只需移动本地指针(指针碰撞),完全无竞争、无锁,快得多。当 TLAB 用完了,再去 Eden 批发一块新的(这次批发才需要同步)。边界情况:大对象(放不进 TLAB 剩余空间)会直接在 Eden 分配;TLAB 快满时剩的小空间可能浪费(叫「TLAB 浪费」)。默认开启(-XX:+UseTLAB)。

详细版

没有 TLAB 的问题

Eden 区是所有线程共享的:
  分配对象 = 移动 Eden 的"分配指针"(指针碰撞:指针后移对象大小)
  多线程同时分配 → 都要改这个指针 → 竞争!
  → 要用 CAS(或加锁)保证原子,高并发下 CAS 频繁失败重试 → 慢

TLAB 的解决

给每个线程分一块私有的 TLAB(在 Eden 里切一小块):
  线程 A 有自己的 TLAB,线程 B 有自己的 TLAB(互不重叠)
  线程 A 分配对象 → 在自己 TLAB 里移动本地指针 → 无竞争、无锁!
  → 大部分对象分配变成"纯本地指针碰撞",极快

TLAB 用完了:
  线程去 Eden "批发"新的一块 TLAB(这一步才需要同步/CAS)
  → 把"每次分配都竞争"变成"每批发一次才竞争一下" → 竞争大幅减少

TLAB 相关概念

概念含义
TLAB 分配在线程私有缓冲区里分配(快,无锁)
慢速分配TLAB 放不下,去 Eden 共享区分配(要同步)
refill(重填)TLAB 用完,重新从 Eden 批发一块
TLAB 浪费(waste)TLAB 快满时剩的空间放不下新对象,被浪费
大对象超过 TLAB 剩余/阈值,直接 Eden 分配

⚠️ TLAB 是「用空间换并发性能」的经典设计——核心思想是「把频繁的细粒度竞争,变成偶尔的粗粒度竞争」:不再是「每分配一个对象就抢一次 Eden 指针」,而是「一次批发一大块(无竞争地分配很多对象),批发时才抢一次」。这和很多并发优化思路一致(如 LongAdder 分段、ThreadLocal 线程隔离、连接池批量预分配)。代价是可能有少量空间浪费(TLAB 尾部剩的小空间、每个线程独占一块导致 Eden 利用率略降),但换来的分配性能提升非常值得。

完整版教学

一、问题背景:Eden 分配的并发竞争

理解 TLAB 要先理解「为什么多线程分配对象会竞争」:

对象分配在哪:新对象绝大多数分配在 Eden 区(年轻代)
怎么分配:指针碰撞(bump-the-pointer)
  - Eden 有一个"分配指针"指向空闲区的起点
  - 分配一个对象 = 把指针往后移动"对象大小",中间那段就是新对象
  - 这样分配极快(就移个指针)

问题:Eden 是所有线程共享的!
  多个线程同时分配对象 → 都要移动同一个分配指针
  → 并发修改同一个指针 = 竞争
  → 必须用 CAS(或锁)保证原子性

高并发下:几十个线程疯狂分配对象、疯狂 CAS 抢指针
  → CAS 频繁失败重试 → 分配变慢 → 成为性能瓶颈

问题的根源是「Eden 共享 + 指针碰撞分配 = 多线程改同一个指针 = 竞争」。指针碰撞本身很快(就移个指针),但多线程抢同一个指针要用 CAS,高并发下 CAS 频繁失败重试,分配就慢了。对象分配是极高频操作(每 new 一个对象就一次),这个竞争在高并发下影响很大。理解「Eden 共享、指针碰撞分配、多线程抢同一指针要 CAS、高并发下 CAS 竞争成瓶颈」,就理解了 TLAB 要解决的问题。

二、TLAB 的核心思想:线程私有缓冲区

TLAB 的解决思路是「给每个线程一块私有缓冲区」:

TLAB = Thread Local Allocation Buffer(线程本地分配缓冲区)

做法:从 Eden 里切出一小块(如 100KB),"分配"给某个线程私有
  - 线程 A 独占它的 TLAB,线程 B 独占它的 TLAB(不重叠)
  - 线程在自己的 TLAB 里分配对象:移动 TLAB 内部的本地指针
    → 只有自己在改这个指针 → 完全无竞争、无需 CAS/锁!

结果:
  绝大多数对象分配 = 在私有 TLAB 里做指针碰撞 = 无锁,极快
  只有"批发新 TLAB"时才需要同步(频率低很多)

TLAB 的精髓是「空间隔离消除竞争」——每个线程有自己的一块 Eden 子区域,在里面分配对象只动自己的本地指针,天然无竞争无锁。这和 ThreadLocal(线程隔离数据)、LongAdder(分段计数)是同一类思路——用「线程私有」把共享竞争消除掉。理解「TLAB 给每个线程私有缓冲区、在里面分配只动本地指针无竞争无锁、是线程隔离消除竞争的思路」,就理解了 TLAB 的核心思想。

三、批发思想:把频繁竞争变偶尔竞争

TLAB 更深层的思想是「批发」——把频繁的小竞争变成偶尔的大竞争:

没 TLAB:每分配 1 个对象,抢 1 次 Eden 指针(零售,频繁竞争)
有 TLAB:一次从 Eden 批发一大块(100KB),
         然后在这块里无竞争地分配很多个对象
         用完了再批发下一块

算例:假设 TLAB 100KB,平均对象 100 字节
  → 一次批发能分配约 1000 个对象
  → 原来 1000 次竞争(每个对象抢一次)
  → 现在 1 次竞争(批发那一次)+ 1000 次无锁本地分配
  → 竞争次数降低约 1000 倍!

「批发」是 TLAB 效果好的关键——一次批发 100KB,就能无竞争地分配约 1000 个对象,把 1000 次竞争压缩成 1 次(只有批发时抢 Eden 指针)。这个「批量预分配、本地消费」的模式在系统设计里到处可见(数据库批量取 ID、连接池预建连接、内存池)。理解「TLAB 用批发把’每个对象抢一次’变成’批发时抢一次’、竞争次数降千倍」,就理解了 TLAB 为什么快——它大幅降低了竞争频率。

四、TLAB 的分配流程

把 TLAB 的完整分配流程走一遍:

线程要 new 一个对象,分配流程:
  1. 尝试在当前线程的 TLAB 里分配(快速路径)
     - TLAB 剩余空间够 → 移动本地指针,分配成功,无锁,结束(最常见)
     - TLAB 剩余空间不够 → 进入下一步

  2. TLAB 放不下了,两条路:
     a. 对象比较小,但 TLAB 剩余不够 → 重填(refill):
        - 丢弃当前 TLAB 剩余(浪费一点),从 Eden 批发新 TLAB
        - 在新 TLAB 里分配(批发要同步)
     b. 对象很大(超过 TLAB 大小或阈值)→ 直接在 Eden 分配(慢速路径,要同步)
        (不值得为一个大对象重填 TLAB)

  3. Eden 也不够 → 触发 Young GC

分配流程分「快速路径(TLAB 内,无锁)」和「慢速路径(Eden 共享,要同步)」:绝大多数分配走快速路径(TLAB 里指针碰撞);TLAB 放不下时,小对象触发重填(丢弃 TLAB 剩余、批发新块),大对象直接去 Eden 分配。理解「TLAB 分配流程:先试 TLAB 快速路径→放不下则小对象重填/大对象直接 Eden→Eden 不够触发 Young GC」,就理解了对象分配的完整决策链。

五、TLAB 的代价:空间浪费

TLAB 不是没有代价的——它「用空间换性能」:

代价一:TLAB 尾部浪费(refill waste)
  - TLAB 快满时,剩余空间放不下新对象,但对象又不算大
  - 是丢弃这点剩余去重填?还是让对象去 Eden 分配?
  - JVM 有个阈值(TLABRefillWasteFraction)权衡:
    剩余空间 > 阈值 → 让大对象去 Eden(不浪费 TLAB 剩余)
    剩余空间 < 阈值 → 丢弃剩余、重填(浪费这点小空间)
  → 无论哪种,都可能有少量浪费

代价二:每个线程独占一块 → Eden 利用率略降
  - 线程多时,很多线程各占一块 TLAB
  - 有些 TLAB 没用满,Eden 有效利用率下降

TLAB 大小是动态调整的:
  - JVM 根据线程的分配速率动态调整每个线程的 TLAB 大小
  - 分配快的线程给大 TLAB,分配慢的给小 TLAB
  - 也可 -XX:TLABSize 手动设,但一般让 JVM 自适应

TLAB 的代价是「少量空间浪费」——尾部剩余放不下对象时的浪费、每个线程独占一块导致 Eden 利用率略降。JVM 用阈值(TLABRefillWasteFraction)权衡「丢弃重填还是去 Eden」,并动态调整 TLAB 大小(按线程分配速率)。这点空间浪费换来的分配性能提升非常值得。理解「TLAB 代价是尾部浪费+Eden 利用率略降、JVM 用阈值权衡并动态调整 TLAB 大小」,就理解了 TLAB 的权衡——空间换并发性能。

六、TLAB 在整个分配体系里的位置

把 TLAB 放到「对象分配」的整体图景里,理解它的位置:

对象分配的完整优化栈(从快到慢):
  ① 栈上分配(逃逸分析,对象不逃逸 → 直接在栈上,随方法返回销毁)
     → 最快,根本不进堆
  ② TLAB 分配(进堆,但在线程私有缓冲区,无锁指针碰撞)
     → 很快,绝大多数对象走这
  ③ Eden 共享分配(TLAB 放不下的,去 Eden 共享区,要同步)
     → 慢一点,需 CAS/锁
  ④ 老年代直接分配(大对象超过阈值,直接进老年代)
     → 见"大对象直接进老年代"

所以 TLAB 是"堆内分配"的第一道快速通道,
配合栈上分配(逃逸分析)、大对象直接进老年代,
构成 JVM 对象分配的完整优化体系

TLAB 是对象分配优化栈的重要一环——栈上分配(逃逸分析)> TLAB 分配(线程私有无锁)> Eden 共享分配(要同步)> 老年代直接分配(大对象)。TLAB 是「堆内分配的第一道快速通道」,绝大多数对象走 TLAB。它和逃逸分析(栈上分配)、大对象处理一起构成 JVM 分配的完整优化体系。理解「TLAB 在分配优化栈里的位置:栈上分配 > TLAB > Eden 共享 > 老年代直接,是堆内分配的快速通道」,就把 TLAB 放进了对象分配的完整图景。

记忆钩子:「TLAB(线程本地分配缓冲区)= 给每个线程在 Eden 里划一块私有缓冲区,在里面分配对象只动本地指针(指针碰撞)无竞争无锁;解决的问题:Eden 共享+指针碰撞→多线程抢同一分配指针要 CAS→高并发慢;核心思想’批发’:一次批发 100KB 就能无竞争分配约 1000 个对象,把’每个对象抢一次’变’批发时抢一次’竞争降千倍;TLAB 放不下→小对象重填(丢弃剩余批发新块)/大对象直接 Eden;代价:尾部浪费+Eden 利用率略降,JVM 动态调 TLAB 大小;分配优化栈:栈上分配>TLAB>Eden 共享>老年代直接;默认开启 -XX:+UseTLAB」

七、常见误区与追问

  • 误区:TLAB 是一块独立于堆的内存。 不是——TLAB 就在 Eden 区里,只是从 Eden 切出来「分配给某个线程私有使用」的一小块;它仍是堆的一部分,只是分配时无竞争。
  • 误区:所有对象都在 TLAB 里分配。 不是——TLAB 放不下的对象(TLAB 剩余空间不够且对象较大、或大对象超过阈值)会走慢速路径直接在 Eden 共享区分配;超大对象还可能直接进老年代。
  • 误区:TLAB 消除了所有分配竞争。 只是大幅降低——TLAB 内分配无竞争,但「批发新 TLAB」时仍要同步抢 Eden 指针;只是批发频率远低于逐个分配,竞争次数降了几个数量级。
  • 误区:TLAB 越大越好。 TLAB 太大→每个线程占的多、Eden 有效利用率低、尾部浪费可能变大、GC 更频繁;太小→重填频繁、批发竞争多;JVM 会按线程分配速率动态调整,一般不用手动设。
  • 追问:TLAB 和栈上分配(逃逸分析)什么关系? 是两个层次的优化——逃逸分析发现对象不逃逸就栈上分配(根本不进堆,最快);进了堆的对象再走 TLAB(线程私有无锁分配)。栈上分配 > TLAB 分配 > Eden 共享分配,层层加速。
  • 追问:TLAB 快满时剩的空间怎么处理? JVM 用 TLABRefillWasteFraction 阈值权衡:如果新对象放不进 TLAB 剩余,且剩余空间较大(超过阈值),就让这个对象去 Eden 共享分配(保留 TLAB 剩余继续用);如果剩余很小,就丢弃剩余、重填一块新 TLAB(浪费这点小空间)。
  • 追问:TLAB 会不会影响 GC? TLAB 里的对象和 Eden 其他对象一样,Young GC 时正常回收;TLAB 只是「分配」时的优化,不改变对象的生命周期和回收;GC 后 Eden 清空,线程会重新批发 TLAB。

八、加强记忆

TLAB(Thread Local Allocation Buffer,线程本地分配缓冲区)是 JVM 加速多线程对象分配的优化——从 Eden 里给每个线程划一块私有缓冲区,线程在自己的 TLAB 里分配对象只需移动本地指针(指针碰撞),无竞争、无锁要解决的问题:对象都分配在 Eden(共享),用指针碰撞分配,多线程同时分配要抢同一个分配指针、用 CAS,高并发下 CAS 频繁失败重试成瓶颈。核心思想是「批发」:一次从 Eden 批发一大块(如 100KB),就能无竞争地分配约 1000 个对象,把「每个对象抢一次」变成「批发时抢一次」,竞争次数降千倍(这和 ThreadLocal 线程隔离、LongAdder 分段是同类思路)。分配流程:先试 TLAB(快速路径,无锁)→ 放不下则小对象重填(丢弃 TLAB 剩余、批发新块)、大对象直接 Eden(慢速路径,要同步)→ Eden 不够触发 Young GC。代价:尾部浪费(TLABRefillWasteFraction 权衡)+ Eden 利用率略降,JVM 动态调整 TLAB 大小在分配优化栈里的位置:栈上分配(逃逸分析)> TLAB(线程私有无锁)> Eden 共享(要同步)> 老年代直接(大对象)。默认开启(-XX:+UseTLAB)。一句话「TLAB = 每个线程在 Eden 里的私有缓冲区,在里面分配只动本地指针无锁;用’批发’把多线程抢 Eden 指针的频繁 CAS 竞争降千倍;放不下则小对象重填/大对象去 Eden,代价是少量空间浪费」。