TLAB对短命大对象完全无效,因其分配判定基于剩余空间一半阈值,大对象绕过TLAB直接走共享Eden区CAS分配,引发竞争抖动与Filler碎片。

TLAB 为什么对短命大对象完全无效
TLAB 只收“小对象”,而所谓“大”,不是看对象绝对大小,而是看它是否超过当前 TLAB 剩余空间的一半。哪怕你 new byte[1024*1024](1MB),只要当前 TLAB 还剩 2MB,它仍可能被塞进去;但若 TLAB 剩余仅 1.8MB,那它立刻被判定为“humongous allocation”,直接跳过 TLAB,走 Eden 共享区 + CAS 分配路径。关键点在于:TLABMaxRefillWasteFraction 控制这个“一半”阈值的计算逻辑,但默认就是 0.5——也就是说,判断依据永远是「剩余空间 × 0.5」,不是总大小。
短命大对象(如临时 byte[]、Protobuf 解析缓冲、Netty PooledDirectByteBuf 的 backing array)往往存活时间极短,但每次分配都触发共享区竞争和填充碎片,这是 TLAB 机制天然无法覆盖的盲区。
用 PrintTLAB 和 jstat 定位真实 refills 与 waste 率
光看 GC 日志里的 ParNew 次数或 Eden 使用率(EU/EC)不够准。必须配合 -XX:+PrintTLAB 实时观察线程级行为:
- 每行日志含
refill requested表示一次 TLAB 耗尽重填,高频出现说明 TLABSize 太小或线程分配节奏突增 -
waste字段显示本次 refill 前剩余未用空间(单位 KB),持续 >100KB 且 refill 频繁,说明 TLABSize 设置过大或对象尺寸分布不均 - 用
jstat -gc对照看EC(Eden 总容量)和EU(已用),若 EU 长期在 EC 的 90%+ 波动,而 PrintTLAB 显示大量线程在 refilling,基本可断定大对象正在把 Eden “顶满”
大对象绕过 TLAB 后的真实代价:CAS 抖动 + Filler Object 碎片
当一个 1.2MB 的 byte[] 绕过 TLAB 直接进 Eden,JVM 不是简单地在 Eden 头部划一块连续内存——它得用 CAS 在共享 Eden 区做原子指针推进。高并发下多个线程同时 CAS 失败重试,会抬高分配延迟,压测时能看到 p99 分配耗时毛刺明显上升。
更隐蔽的是 Filler Object:JVM 为对齐内存,会在大对象后插入一段 int[] 或 Object 填充体,这些 filler 不参与业务逻辑,却占堆、进 GC 扫描范围。它们不会立即回收,而是随 Eden 一起在下次 Minor GC 时被清理,但会拉长扫描链表、增加 GC 暂停时间。
验证方式:用 -XX:+PrintGCDetails 查日志中是否有 humongous allocation 字样;再用 Async-Profiler 的 alloc 事件采样,看 top 分配热点是否集中在 byte[]、char[] 等数组构造上。
调优不是设个固定 TLABSize 就完事
硬写 -XX:TLABSize=512k 是典型拍脑袋做法。TLAB 大小必须匹配线程实际分配压力:
- 单线程每秒分配峰值约 600KB → TLABSize 设 512k 可能刚够,但若突发到 900KB,就会频繁 refill
- 线程数超 200 的服务,TLAB 总内存占用 = 线程数 × TLABSize,设太大易引发 Eden 空间挤压,间接导致 Minor GC 提前
- 推荐先开
-XX:+ResizeTLAB(默认 true),让 JVM 根据各线程 refill 频率自动扩缩;再用-XX:TLABWasteTargetPercent=1把平均浪费率压到 1% 以内 - 大促稳态场景(如电商秒杀)可关自适应:
-XX:-ResizeTLAB -XX:TLABSize=512k,避免 resize 时的短暂同步开销
真正难处理的,从来不是 TLAB 开不开,而是大对象尺寸分布和线程分配节奏之间那层模糊的匹配关系——它没法靠一个参数解决,得靠 PrintTLAB 日志 + jstat + 分配采样三者交叉验证。










