关闭tlab后高并发分配明显变慢,是因为多线程必须竞争eden区同一top指针,依赖cas自旋同步,冲突率可达30%以上;而tlab使分配退化为线程本地指针加法,无锁无原子操作,实测耗时从1320ms降至480ms。

TLAB 不是“开了就快”,而是必须结合分配行为、对象大小分布和 GC 日志一起看,否则容易误判效果甚至调优反向。
为什么关闭 TLAB 后高并发分配明显变慢
多个线程同时在 Eden 区分配对象时,必须竞争修改同一个 top 指针。JVM 用 CAS 自旋或轻量锁同步这个过程,但高并发下失败重试成本陡增——尤其当对象小、创建频次高(如循环 new byte[64])时,CAS 冲突率可能超 30%。TLAB 把这个全局竞争,拆成每个线程只动自己的 top 和 end 指针,分配退化为纯指针加法,无锁、无原子操作。
实测对比(JDK 17,Eden 256MB):
- 开启
-XX:+UseTLAB:分配 1 亿个byte[32]耗时约480ms - 关闭
-XX:-UseTLAB:同样操作耗时升至1320ms,且 GC 日志中Allocation Failure频次翻倍
如何确认你的应用真正在用 TLAB
仅靠 -XX:+UseTLAB(默认已开)不等于生效。关键要看实际分配是否落入 TLAB —— 这取决于对象大小、TLAB 剩余空间、以及 JVM 的废弃策略。
启用诊断输出:
-XX:+PrintGCDetails -XX:+PrintTLAB
观察 GC 日志中的关键字段:
-
TLAB: gc thread: 0x... desired_size: 256K actual_size: 248K→ 实际分配的 TLAB 大小 -
waste: 12.4KB (5.0%)→ 当前 TLAB 废弃空间占比,超过-XX:TLABWasteTargetPercent(默认 1%)会触发重填 -
refill waste: 2.1MB→ 所有线程因浪费超标而重填 TLAB 导致的 Eden 浪费总量
如果日志里频繁出现 refill waste 远高于 allocation,说明 TLAB 太小或对象尺寸波动大,-XX:+ResizeTLAB(默认开)没跟上节奏。
TLAB 大小调优不能只盯一个参数
硬设 -XX:TLABSize=512K 很危险:小线程撑不住,大线程又浪费 Eden。JVM 动态调整才是正解,但需配合监控反馈。
影响 TLAB 实际大小的因子有三个:
-
-XX:TLABWasteTargetPercent=1:控制单次 TLAB 废弃上限,默认 1%,值太小(如 0.1)会导致重填太勤;太大(如 5)则 Eden 利用率下降 -
-XX:TLABRefillWasteFraction=64:决定线程申请新 TLAB 前,允许浪费多少“旧 TLAB 剩余空间”,默认 1/64 ≈ 1.56%,和上面参数协同作用 - 线程分配速率:JVM 根据每个线程过去 GC 周期内的分配字节数自动估算下一轮 TLAB 大小,所以负载突增时会有短暂滞后
典型陷阱:Web 应用中,IO 线程(Netty EventLoop)分配极少,但业务线程(Spring MVC)高频创建 DTO,若共用同一套默认估算逻辑,IO 线程的 TLAB 可能被设得过大,白白占用 Eden。
哪些对象根本不会进 TLAB
TLAB 只服务“小对象”。一旦对象大小超过当前线程 TLAB 剩余空间,或本身尺寸超过 JVM 认定的“大对象阈值”,就会绕过 TLAB,直奔 Eden 共享区 —— 此时锁竞争照旧。
判断依据:
- 对象大小 >
MinTLABSize(默认 2KB)且 > 当前 TLAB 剩余空间 → 走共享 Eden - 对象大小 >
MaxTLABSize(由TLABSize和TLABWasteTargetPercent推导)→ 强制走共享 Eden - 显式使用
-XX:-UseTLAB或通过-XX:+AlwaysPreTouch预触内存后,部分 JVM 版本会禁用 TLAB 分配
常见误判场景:用 new byte[8192] 测试 TLAB 效果,结果发现关闭/开启参数性能差异极小——因为该对象大概率已超出默认 TLAB 容量,压根没走 TLAB 路径。
TLAB 的价值不在“存在”,而在“适配”:它对齐的是线程真实的分配节奏,而不是堆的静态配置。最常被忽略的一点是——TLAB 浪费率(waste%)稳定在 0.8%~1.2% 之间,通常就是最佳状态;低于 0.5% 往往意味着过度保守,高于 2% 则说明线程分配模式已发生偏移,需要重新观察 GC 日志中各线程的 refill 行为,而非盲目调大 TLABSize。










