TLAB:为什么 new Object() 这么快?
一句话结论(30s)
TLAB 的本质是 JVM 在 Eden 区为每个线程预分配的一块私有内存(默认占 Eden 的 1%),把共享堆上的并发分配转化为线程内私有分配。因为 TLAB 内分配是纯指针碰撞、单条 CPU 指令完成,无需 CAS 也无需锁,所以多线程 new 对象几乎无锁;只有 TLAB 用完时才 CAS 争抢新 TLAB。权衡是它以牺牲少量 Eden 碎片为代价换取极致的分配速度,而且只加速分配、不解决回收。
核心原理(2min)
对象分配路径是三道防线:TLAB 内指针碰撞(极速)→ TLAB 用完时 CAS 从共享空闲区抢新 TLAB → 大对象直接进堆;配合逃逸分析,NoEscape 的局部对象可进一步栈上分配(方法返回自动清理),这是第二道防线,让部分对象根本不用进堆。JVM 会按各线程分配速率动态调整 TLAB 大小——分配快的变大以减少 CAS 频率、分配慢的变小以减少 Eden 碎片化,可用 -XX:+PrintTLAB 观察每个线程的统计。
底层深入(5-10min)
对象分配的并发挑战
堆是所有线程共享的。多个线程同时 new 对象 → 并发争抢堆内存 → 需要 CAS 或锁保证分配正确性 → 并发分配成为瓶颈。
想一想:为什么共享堆上并发 new 会成为瓶颈?因为多个线程抢同一块内存,必须靠 CAS 或锁保证”这块地址只被分配一次”,竞争一激烈,锁就成了吞吐的天花板。
TLAB 的解决
JVM 在 Eden 区为每个线程预分配一块私有内存——TLAB(Thread Local Allocation Buffer,默认占 Eden 的 1%):
Eden 区:
[TLAB-Thread1][TLAB-Thread2][...][共享空闲区]
每个线程在自己的 TLAB 内分配对象:
ptr += object_size; // 指针碰撞,单条指令!
if (ptr > TLAB_end) // TLAB 用完
→ CAS 从共享区抢新的 TLAB
TLAB 内的分配是纯指针碰撞——不需要 CAS,不需要锁,一条 CPU 指令完成。 只有 TLAB 用完时才需要 CAS 争抢新的 TLAB。
想一想:为什么 TLAB 内分配不需要 CAS?因为这块内存是线程私有的,只有本线程会写它的 top 指针,根本没有竞争,自然不需要同步;把”全局竞争”变成”私有独占”,CAS 次数就从”每个对象一次”降到”每块 TLAB 一次”。
TLAB vs 栈上分配
| TLAB | 栈上分配 | |
|---|---|---|
| 位置 | Eden 内私有块 | 线程栈帧内 |
| 触发条件 | 所有堆分配 | 逃逸分析判定 NoEscape |
| 对象回收 | GC | 方法返回自动清理 |
| 适用 | 大部分新对象 | 不逃逸的局部对象 |
TLAB 是一道防线(让堆分配变快),栈上分配是二道防线(让部分对象不进堆)。
TLAB 的动态调整
JVM 根据线程的分配速率动态调整 TLAB 大小:
- 分配速率高 → TLAB 变大(减少 CAS 抢新 TLAB 的频率)
- 分配速率低 → TLAB 变小(减少 Eden 碎片化)
想一想:为什么分配快的线程 TLAB 反而要”变大”?因为 TLAB 越大、用完越慢,抢新 TLAB 的 CAS 频率越低,分配快的线程就更少被 CAS 打断;分配慢的线程 TLAB 小,避免大块内存用不满、在 Eden 里留下碎片。
-XX:+PrintTLAB 可以看到每个线程的 TLAB 统计。
总结
TLAB = 线程私有的 Eden 碎片。对象分配路径:TLAB内指针碰撞(极速) → TLAB用完CAS抢新TLAB → 大对象直接进堆。配合逃逸分析的栈上分配,JVM 的对象分配已经是高度优化的无锁流程。
章末提问
追问 1:TLAB 为什么能让对象分配”几乎无锁”?
回答思路:结论先行——因为它把共享堆上的并发分配转成线程内私有分配。因为 JVM 给每个线程在 Eden 预分配一块私有 TLAB,本线程在 TLAB 内用指针碰撞(单条指令后移 top 指针)分配,没有竞争所以无需 CAS/锁;只有 TLAB 用完才 CAS 抢新 TLAB,频率远低于”每个对象都 CAS”。
追问 2:TLAB 和栈上分配(逃逸分析)是一回事吗?
回答思路:结论先行——不是,它们是两道不同防线。因为 TLAB 只是”让堆分配变快”,对象最终还是进堆、由 GC 回收;栈上分配针对 NoEscape 对象,直接在栈帧里分配、方法返回自动清理,根本不用进堆。TLAB 覆盖几乎所有新对象,栈上分配只覆盖不逃逸的局部对象。
追问 3:TLAB 有什么代价?
回答思路:结论先行——牺牲少量 Eden 碎片,且只加速分配、不解决回收。因为每个线程的 TLAB 可能用不满就换新的,块与块之间会留碎片;同时 TLAB 只影响分配路径,对象回收仍靠 GC,所以它解决的是”分配瓶颈”而非”回收瓶颈”。