Appearance
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
Java是一种面向对象、跨平台的语言,其对象、内存这类内容一直是难点。所以,即使是Java初学者,也或多或少对JVM有一些了解。可以说,JVM相关知识是每个Java开发者必学的知识点,也是面试必考的知识点。
在JVM的内存结构中,常见的两个区域是堆内存和栈内存。如无特指,本文提到的栈均指虚拟机栈。关于堆和栈的区别,很多开发者如数家珍。很多书籍或网上的文章大多是这样介绍的:
1、堆是线程共享的内存区域,栈是线程独享的内存区域。 2、堆中主要存放对象实例,栈中主要存放各种基本数据类型、对象的引用。
以上两个结论均不是完全正确的。
本文首先解释为什么“堆是线程共享的内存区域,栈是线程独享的内存区域。”这句话并不完全正确。关于JVM内存结构的相关知识,大家可以阅读JVM内存结构 VS Java内存模型 VS Java对象模型、万万没想到,JVM内存结构的面试题可以问的这么难?这类文章。
Java对象的内存分配过程是如何保证线程安全的?
我们知道,Java是一门面向对象的语言,我们在Java中使用的对象都需要被创建出来。创建一个对象的方法有很多种,但是无论如何,对象在创建过程中都需要分配内存。
对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后初始化。
但是,因为堆是全局共享的,因此同一时间可能有多个线程在堆上申请空间。在并发场景中,两个线程可能先后把对象引用指向同一个内存区域,这就产生了并发问题。

为了解决这个并发问题,对象的内存分配过程就必须使用同步控制。但是我们都知道,无论是使用哪种同步方案(虚拟机使用的可能是CAS),都会影响内存的分配效率。
而Java对象的分配是Java中的高频操作,所以,人们想到另外一个办法来提升效率。这里我们重点说一个HotSpot虚拟机的方案:
每个线程在Java堆中预先分配一小块内存。给对象分配内存时,直接在自己这块”私有”内存中分配。这部分区域用完之后,再分配新的”私有”内存。
这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
什么是TLAB
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。启用TLAB功能后,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用。这样每个线程都单独拥有一个空间,需要分配内存时就在自己的空间上分配。因此不存在竞争,可以大大提升分配效率。
上面的描述中有”线程专属”、”只给当前线程使用”、”每个线程单独拥有”这样的说法。
所以说,因为有了TLAB技术,堆内存并不是完完全全的线程共享。其eden区域中还是有一部分空间是分配给线程独享的。
这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独占的。在读取、垃圾回收这类动作上都是线程共享的,而且在使用上也没有什么区别。

即:每个线程在初始化时都会去堆内存中申请一块TLAB。但这不代表这个TLAB区域的内存其他线程就完全无法访问。其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。
并且,在TLAB分配之后,并不影响对象的移动和回收。即:对象刚开始可能通过TLAB分配内存,存放在Eden区。但是对象还是会被垃圾回收,或者被移到Survivor Space、Old Gen这类区域。

还有一点需要注意的是,我们说TLAB是在eden区分配的,因为eden区域本身就不太大。而且TLAB空间很小,默认情况下仅占有整个Eden空间的1%。所以,必然存在一些大对象是无法在TLAB直接分配。
遇到TLAB中无法分配的大对象,对象还是可能在eden区或者老年代这类区域分配的。但是这种分配就需要同步控制。这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。
TLAB带来的问题
虽然在一定程度上,TLAB大大的提升了对象的分配速度,但是TLAB并不是就没有任何问题的。
前面我们说过,因为TLAB内存区域并不是很大,所以有可能经常不够用。在《实战Java虚拟机》中有这样一个例子:
比如一个线程的TLAB空间有 100 KB,其中已经使用了 80 KB。当需要再分配一个 30 KB 的对象时,就无法直接在TLAB中分配。这时有两种处理方案:
如果对象所需空间超过TLAB剩余空间,直接在堆内存中分配。
如果对象所需空间超过TLAB剩余空间:废弃当前TLAB,重新申请。在新TLAB中再次分配内存。
以上两个方案各有利弊。如果采用方案1,那么就可能存在一种极端情况:TLAB只剩下 1 KB。这会导致后续需要分配的大多数对象都需要在堆内存直接分配。
如果采用方案2,也有可能频繁废弃TLAB、频繁申请TLAB。虽然在TLAB上分配内存是线程独享的,但是TLAB内存从堆中划分出来的过程确实可能存在冲突。所以,TLAB的分配过程也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。
为了解决这两个方案存在的问题,虚拟机定义了一个refill_waste的值,这个值可以翻译为“最大浪费空间”。
当请求分配的内存大于refill_waste的时候,会选择在堆内存中分配。若小于refill_waste值,则会废弃当前TLAB,重新创建TLAB,再次分配对象内存。
前面的例子中,TLAB总空间 100 KB,使用了 80 KB,剩余 20 KB。如果refill_waste的值为 25 KB:新对象的内存大于 25 KB 时,直接在堆内存分配。小于 25 KB 时,废弃之前的那个TLAB,重新分配一个TLAB空间,给新对象分配内存。
TLAB使用的相关参数
TLAB功能是可以选择启用或者禁用的,可以通过设置-XX:+/-UseTLAB参数来指定是否启用TLAB分配。
TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳的运行状态。如果需要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB来禁用,并且使用-XX:TLABSize来手工指定TLAB的大小。
TLAB的refill_waste也是可以调整的,默认值为64,即表示使用约为1/64空间大小作为refill_waste,使用参数:-XX:TLABRefillWasteFraction来调整。
如果想要观察TLAB的使用情况,可以使用参数-XX:+PrintTLAB 来跟踪。
总结
为了保证对象的内存分配过程中的线程安全性,HotSpot虚拟机提供了一种叫做TLAB(Thread Local Allocation Buffer)的技术。
在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用。当需要分配内存时,就在自己的空间上分配,这样就不存在竞争,可以大大提升分配效率。
所以,“堆是线程共享的内存区域”这句话并不完全正确。因为TLAB是堆内存的一部分,TLAB在读取上确实是线程共享的。但是在内存分配上,TLAB是线程独享的。
TLAB的空间并不大,所以大对象还是可能需要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代。然后再确定是在eden分配还是在老年代分配。
