TLAB 与内存分配策略
TLAB 的前世今生
TLAB(Thread-Local Allocation Buffer)是 HotSpot 虚拟机为解决多线程并发分配对象而引入的优化。它的核心思想很朴素:每个线程有自己的「地盘」,在自己的地盘里干活,不需要和别人协调。
为什么需要 TLAB
没有 TLAB 时,多个线程同时在 Eden 区分配对象:
java
// 线程 A 和线程 B 同时分配对象
// 如果没有同步机制:
ThreadA: 指针 = 0x1000, 分配 100B, 移动指针到 0x1100
ThreadB: 指针 = 0x1000, 分配 100B, 移动指针到 0x1100 ← 覆盖了 A 的对象!解决方法是加锁:每次分配都加一个全局锁。但这意味着所有线程都在抢同一把锁,高并发时性能急剧下降。
TLAB 完美解决了这个问题。
TLAB 的工作原理
┌─────────────────────────────────────────────────────────────┐
│ Eden 区 │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Thread A │ │ Thread B │ │ Thread C │ │
│ │ TLAB │ │ TLAB │ │ TLAB │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Obj A1│ │ │ │ Obj B1│ │ │ │ Obj C1│ │ │
│ │ │ Obj A2│ │ │ │ Obj B2│ │ │ └───────┘ │ │
│ │ │(继续) │ │ │ │(继续) │ │ │ │ │
│ │ └───────┘ │ │ └───────┘ │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ ↑ │
│ 未分配的 TLAB 区域 │
└─────────────────────────────────────────────────────────────┘每个线程在 Eden 区预先分配一块 TLAB(通常 1~2KB)。对象在 TLAB 内分配,当 TLAB 用完时,申请新的 TLAB——这个申请需要加锁,但申请频率远低于每次分配都加锁。
TLAB 相关参数
bash
# 是否启用 TLAB(JDK 6 Update 23+ 默认启用)
java -XX:+UseTLAB MyApp # 启用
java -XX:-UseTLAB MyApp # 禁用(不推荐)
# TLAB 初始大小
java -XX:TLABSize=512k MyApp # TLAB 初始大小 512KB
# TLAB 最大大小
java -XX:TLABRefillWasteFraction=100 MyApp
# 含义:TLAB 最大浪费空间 = 1/100 的 TLAB 大小
# 如果剩余空间小于这个阈值,放弃这块 TLAB,申请新的
# TLAB 统计信息
java -XX:+PrintTLAB MyApp
# 输出类似:
# TLAB: gc: 12 avg: 2048kg 0.80% avg: 16384 ref: 0 waste% 0.0 size: 65528 max: 262144TLAB 的优缺点
优点
| 优点 | 说明 |
|---|---|
| 无锁分配 | 线程在自己 TLAB 中分配,无需任何同步 |
| 缓存友好 | TLAB 通常是连续内存,CPU 缓存命中率高 |
| 减少碎片 | GC 负责整理 Eden 区,应用线程不关心碎片 |
缺点
| 缺点 | 说明 |
|---|---|
| 内存浪费 | TLAB 申请后未用满就放弃,浪费空间 |
| 不适合大对象 | TLAB 通常只有几 KB,大对象无法使用 |
| 参数调优复杂 | TLAB 大小需要根据应用特性调整 |
TLAB 与大对象
当对象大小超过 TLAB 剩余空间时,JVM 有两种选择:
java
public class TLABOverflow {
public static void main(String[] args) {
// 场景:对象大于 TLAB 剩余空间
// JVM 的处理策略:
// 小对象(< TLAB 剩余):正常在 TLAB 分配
byte[] small = new byte[256]; // 在当前 TLAB 内分配
// 如果 TLAB 剩余不足以容纳:
// 策略一(默认):放弃当前 TLAB,申请新的
// 策略二(-XX:-ZeroTLAB):直接在 Eden 区分配
byte[] large = new byte[1024]; // 可能触发新 TLAB 申请
}
}逃逸分析与 TLAB 的关系
逃逸分析(Escape Analysis)是一种 JIT 编译器优化,可以分析对象的生命周期范围。如果一个对象不逃逸出方法或线程,就可以:
- 栈上分配:对象直接在栈上分配,不需要堆分配,也就不需要 TLAB
- 标量替换:对象被拆解成多个标量(基本类型),直接存在寄存器或栈上
java
public class EscapeAnalysis {
public static void main(String[] args) {
// JIT 编译器做逃逸分析后
// method() 的局部变量不逃逸,可以在栈上分配
for (int i = 0; i < 1000000; i++) {
method();
}
}
static void method() {
// point 对象没有逃逸(不会被方法外部引用)
// JIT 可能直接在栈上分配它,而不需要在堆上分配
Point p = new Point(1, 2);
int sum = p.x + p.y;
// method() 返回后,p 自动销毁,不需要 GC
}
}
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}关于逃逸分析的详细内容,我们会在 逃逸分析(栈上分配/同步省略/标量替换) 中深入讲解。
分配策略总结
| 分配方式 | 适用场景 | 同步需求 |
|---|---|---|
| TLAB | 普通对象(几字节到几十KB) | 无(线程私有) |
| Eden 指针碰撞 | TLAB 禁用时 / TLAB 放不下 | 需要(全局) |
| 老年代直接分配 | 大对象 / TLAB overflow | 需要(全局) |
| 栈上分配 | 逃逸分析确定不逃逸的对象 | 无(栈上自动管理) |
本节小结
TLAB 是 HotSpot 虚拟机分配优化的核心机制:
- 思想:线程本地缓冲区,避免分配时加锁
- 工作方式:每个线程在 Eden 区有自己的专属 TLAB
- 适用对象:大多数普通对象
- 相关参数:
-XX:+UseTLAB、-XX:TLABSize、-XX:TLABRefillWasteFraction
TLAB 让 Java 的堆分配速度接近 C 的 malloc,甚至接近栈分配。结合逃逸分析,JIT 编译器还能把不逃逸的对象直接分配在栈上,进一步提升性能。
下一节,我们来看 逃逸分析(栈上分配/同步省略/标量替换)。
