TLAB refill失败时,JVM尝试为线程重新分配新TLAB但Eden空间不足,触发Young GC;失败后线程降级至共享Eden分配,导致内存不连续、填充对象堆积和碎片化损耗。

TLAB refill 失败时到底发生了什么
当线程的 TLAB 空间不足,且剩余空间又不足以容纳当前对象(哪怕只差几个字节),JVM 就会触发 refill:尝试为该线程申请一块新的 TLAB。但这个过程不是原子的——它要先锁住 Eden 区的分配指针,CAS 更新,再划出新缓冲区。在高并发、多核密集写场景下,多个线程几乎同时 refill,就会在 Eden 分配指针上形成 CAS 冲突队列,出现“refill 拥塞”。此时部分线程被迫降级走慢路径,直接在共享 Eden 区分配,引发局部内存不连续、填充对象(Filler Object)堆积,也就是你看到的“碎片化损耗”。
- 典型现象:
PrintTLAB日志中某线程refills次数突增,同时waste字段飙升(比如从 0.5% 跳到 12%) - 根本原因不是 TLAB 太小,而是 refill 频率与线程分配节奏错配:线程每秒分配 300KB,但 TLAB 设成 128KB,导致每秒 refill 2~3 次,每次浪费几十 KB
- 不要只看平均分配量;重点抓 P95/P99 单次请求的对象体积分布——大促流量里常有突发的 400KB 临时 buffer,它会让一个本该撑 5 秒的 TLAB 在 0.8 秒内报废
用 -XX:+PrintTLAB 定位真实浪费源头
-XX:+PrintTLAB 输出不是统计摘要,而是每秒一条线程级明细,必须配合真实流量跑够 3~5 分钟才能看出模式。关键字段就三个:refills(本秒 refill 次数)、waste(本秒浪费字节数 / 本秒总分配字节数 × 100%)、fast_alloc(本秒成功 TLAB 分配次数)。如果某线程 waste > 5% 且 refills > 1,基本可判定其 TLAB 过小或分配不均。
- 别信“平均 waste 1.2%”——可能 90% 线程 waste 是 0.3%,剩下 10% 线程拉高均值到 1.2%,而这 10% 才是 GC 波动源
- 对比
jstat -gc <pid></pid>的EC(Eden 容量)和EU(Eden 已用),若 EU 长期在 EC 的 70%~90% 波动,但 PrintTLAB 显示大量 refill,说明碎片已开始侵蚀有效分配空间 - 开启
-XX:+PrintGCDetails后,留意日志里是否频繁出现humongous allocation——这代表你试图 new 的对象(如new byte[1024*1024])被判定为大对象,绕过 TLAB 直接冲击 Eden 头部,加剧竞争
TLABSize 和 ResizeTLAB 的实际取舍逻辑
硬设 -XX:TLABSize=512k 只在两种情况成立:线程分配速率极其稳定(如固定 batch size 的 IO worker),或大促压测阶段需消除 resize 开销。其余场景,默认开启 -XX:+ResizeTLAB(JDK8+ 默认 true)更安全——JVM 会基于最近几次 GC 周期内各线程的实际分配总量,动态调整下一轮的 TLAB 初始大小。
- 自适应生效前提:必须让 JVM 经历至少 2 次 Young GC,它才开始积累线程分配画像;压测时间太短(
- 控制浪费上限:加
-XX:TLABWasteTargetPercent=1,JVM 会把 refill 触发阈值压得更紧,宁可多 refill 一次,也不让单次浪费超 1% - 注意
MinTLABSize:默认 2048 字节,但某些轻量线程(如心跳检测线程)每秒只分配几百字节,JVM 不会把它 TLAB 缩到低于此值,造成隐性浪费——此时应考虑用线程池隔离这类低频线程
大对象与 TLAB 碎片的隐蔽耦合
所谓“大对象绕过 TLAB”,真实逻辑是:只要对象大小 > 当前 TLAB 剩余空间的一半,就拒绝进入。这意味着一个刚 refill 出来、还剩 400KB 的 TLAB,遇到一个 201KB 对象就会直接拒收,转而走共享 Eden 分配。这种“半途拒收”比 TLAB 耗尽更伤——它留下 199KB 碎片无法复用,又不触发 refill,等于白占内存。
- 监控建议:用
Async-Profiler的alloc模式采样堆分配热点,重点关注 top 3 大对象的 class 和 size,而不是盲目调大 TLAB - 避免“拆分陷阱”:把
new byte[1024*1024]改成 10 个new byte[1024*102],看似进了 TLAB,实则引发 10 次 refill + 10 个 Filler Object,碎片总量反而上升 - 真正解法是分级:对确定的大 buffer,改用
ByteBuffer.allocateDirect()或池化;对不确定的,用-XX:TLABMaxRefillWasteFraction=0.5收紧判断逻辑(但需验证是否增加 refill 频率)
refill 次数稳定但 waste 持续爬升——说明你的线程正在缓慢地、不可逆地把 Eden 区割成越来越细的格子。

















