Skip to content

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: 262144

TLAB 的优缺点

优点

优点说明
无锁分配线程在自己 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 编译器还能把不逃逸的对象直接分配在栈上,进一步提升性能。

下一节,我们来看 逃逸分析(栈上分配/同步省略/标量替换)

基于 VitePress 构建