Java中的TLAB或线程本地分配缓冲区是什么?

2025/04/26

1. 简介

在本教程中,我们将介绍线程本地分配缓冲区(TLAB),我们将了解它们是什么、JVM如何使用它们以及如何管理它们。

2. Java中的内存分配

Java中的某些命令会分配内存,最明显的是new关键字,但还有其他命令,例如使用反射

每当我们执行此操作时,JVM都必须在上为新对象留出一些内存。具体来说,JVM内存分配在Eden区(或Young区)以这种方式进行所有分配

在单线程应用程序中,这很容易,由于任何时候都只能发生一个内存分配请求,因此线程可以简单地获取下一个合适大小的块,这样就完成了:

然而,在多线程应用程序中,我们不能这么简单地处理。如果这样做,就存在两个线程在同一时刻请求内存,并分配到同一个块的风险:

为了避免这种情况,我们同步内存分配,以便两个线程不能同时请求同一个内存块。但是,同步所有内存分配会使它们本质上变成单线程的,这可能会成为我们应用程序的巨大瓶颈。

3. 线程本地分配缓冲区

JVM使用线程本地分配缓冲区(TLAB)来解决这个问题,这些是为特定线程保留的堆内存区域,仅供该线程分配内存

通过这种方式工作,无需同步,因为只有一个线程可以从该缓冲区拉取数据,缓冲区本身是以同步方式分配的,但这种操作不太频繁。

由于为对象分配内存是比较常见的情况,因此这可以带来巨大的性能提升,但具体提升多少呢?我们可以通过一个简单的测试轻松确定:

@Test
public void testAllocations() {
    long start = System.currentTimeMillis();

    List<Object> objects = new ArrayList<>();

    for (int i = 0; i < 1_000_000; ++i) {
        objects.add(new Object());
    }

    Assertions.assertEquals(1_000_000, objects.size());

    long end = System.currentTimeMillis();
    System.out.println((end - start) + "ms");
}

这是一个相对简单的测试,但它确实能完成任务。我们将为1000000个新的Object实例分配内存,并记录所需的时间。然后,我们可以多次运行该测试,分别启用和禁用TLAB,看看平均耗时是多少(我们将在第5节中介绍如何关闭TLAB)。

我们可以清楚地看到差异,启用TLAB的平均时间为33毫秒,而未启用TLAB的平均时间则上升至110毫秒,仅仅更改这一项设置,就提升了230%

3.1 TLAB空间不足

显然,我们的TLAB空间是有限的,那么,当TLAB空间用完时会发生什么呢

如果我们的应用程序尝试为新对象分配空间,而TLAB没有足够的可用空间,则JVM有四种可能的选择:

  1. 它可以为该线程分配新的TLAB空间,从而有效地增加可用空间量。
  2. 它可以从TLAB空间外部为该对象分配内存。
  3. 它可以尝试使用垃圾回收器释放一些内存。
  4. 它可能无法分配内存,而是会引发错误。

选项4是我们的灾难性情况,因此我们希望尽可能避免它,但如果其他情况不会发生,它也是一个选择。

JVM使用一系列复杂的启发式算法来确定使用其他选项中的哪一个,并且这些启发式算法可能会在不同的JVM和不同版本之间发生变化,但是,影响这一决策的最重要细节包括

  • 一段时间内可能发生的分配次数,如果我们可能分配大量对象,那么增加TLAB空间将是更高效的选择。如果我们可能分配很少的对象,那么增加TLAB空间实际上可能效率较低。
  • 请求的内存量,请求的内存越多,在TLAB空间之外分配内存的成本就越高。
  • 可用内存量,如果JVM有大量可用内存,则增加TLAB空间比内存使用率过高时要容易得多。
  • 内存争用量,如果JVM中有大量线程,且每个线程都需要内存,那么增加TLAB空间的成本可能比线程数很少时高得多。

3.2 TLAB容量

使用TLAB似乎是提升性能的绝佳方法,但总是有成本的。为了防止多个线程分配同一块内存区域,需要进行同步,这使得TLAB本身的分配成本相对较高。如果JVM内存使用率特别高,我们可能还需要等待有足够的内存可供分配,因此,理想情况下,我们希望尽可能少地执行此操作。

但是,如果一个线程为其TLAB空间分配的内存量超出其实际需求,那么这些内存就会闲置在那里,基本上被浪费了。更糟糕的是,这些空间的浪费会使其他线程更难获得TLAB空间的内存,从而导致整个应用程序的整体运行速度变慢。

因此,关于究竟应该分配多少空间存在争议。分配太多,就会浪费空间;分配太少,分配TLAB空间的时间就会超出预期。

值得庆幸的是,JVM将为我们处理所有这些,但我们很快就会看到如何在必要时根据我们的需要对其进行调整。

4. 查看TLAB使用情况

现在我们知道了TLAB是什么以及它对我们的应用程序的影响,我们如何才能看到它的实际作用呢

不幸的是,jconsole工具并不像标准内存池那样提供任何可见性。

但是,JVM本身可以输出一些诊断信息,这使用了新的统一GC日志记录机制,因此我们必须使用-Xlog:gc+tlab=trace标志启动JVM才能查看这些信息。这将定期打印JVM当前TLAB使用情况的信息,例如,在GC运行期间,我们可能会看到类似这样的信息:

[0.343s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000000014000a600 [id: 10499] desired_size: 450KB slow allocs: 4  refill waste: 7208B alloc: 0.99999    22528KB refills: 42 waste  1.4% gc: 161384B slow: 59152B

这告诉我们,对于这个特定的线程:

  • 当前TLAB大小为450KB(desired_size)。
  • 自上次GV以来,TLAB之外已有4次分配(慢速分配)。

请注意,确切的日志记录会因JVM和版本而异。

5. 调整TLAB设置

我们已经了解了打开和关闭TLAB可能带来的影响,但我们还能用它做什么呢?我们可以在启动应用程序时通过提供JVM参数来调整许多设置

首先,让我们实际看看如何关闭它,这可以通过传递JVM参数-XX-UseTLAB来实现,设置该参数将停止JVM使用TLAB,并强制它在每次内存分配时使用同步。

我们也可以保持TLAB启用,但通过设置JVM参数-XX:-ResizeTLAB来阻止其调整大小。这样做意味着,如果某个线程的TLAB已满,则所有后续的分配都将在TLAB之外进行,并需要同步。

我们还可以配置TLAB的大小,我们可以为JVM参数-XX:TLABSize指定一个值,该参数定义了JVM为每个TLAB建议的初始大小,也就是每个线程需要分配的大小。如果将其设置为0(默认值),JVM将根据JVM的当前状态动态确定每个线程需要分配的大小。

我们还可以指定-XX:MinTLABSize来为每个线程的TLAB大小设置下限,以便在允许JVM动态确定大小的情况下使用,我们还可以使用-XX:MaxTLABSize来为每个线程的TLAB大小设置上限。

所有这些设置都已具有合理的默认值,通常最好只使用这些设置,但如果我们发现存在问题,也可以有一定程度的控制

6. 总结

在本文中,我们了解了线程本地分配缓冲区的概念、它们的使用方法以及如何管理它们。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章