最近在看java性能相关方面的书籍。然后在GC调优相关的部分出现了,线程本地分配缓冲区的名词,对于它的调优级为重要,所以就梳理一下这个到底是什么?为什么他对于JVM性能如此重要。
什么是JVM线程本地分配缓冲区(Thread Local Allocation Buffer)TLAB?
JVM在分配新对象时,将在专用于原始线程的TLAB(线程本地分配缓冲区)中分配该对象。由于每个线程只能写入自己的TLAB,因此不需要同步。TLAB默认情况下处于启用状态,可以随时使用-XX:-UseTLAB调整标志将其禁用(除非你想找刺激)。
TLAB对象在一个共享的空间中分配,我们需要采用些同步机制来管理空间内的空闲空间指针。每个线程有固定的分配区域,分配对象时,线程之间不需要进行任何的同步。(这是利用线程本地变量避免锁争用的)。
TLAB是如何分配的?
通常,TLAB的使用对程序员和终端用户而言是完全透明的:默认情况下TLAB就是开启的,JVM管理着它们的大小及如何使用。我们需要意识到的最重要的事是TLAB都不大,因此大型对象无法在TLAB内进行分配。大型对象必须直接从堆上分配,由于需要同步,一旦TLAB空间用尽,特定大小的对象就无法再继续分配。这时,JVM可以有不同的选择。
- 一个选项是回收这块TLAB,为该线程分配一块新的TLAB。由于TLAB只是Eden空间中的一个区段,下一次新生代垃圾收集时这块TLAB整个都会被回收,并在之后的空间分配中重用。
- JVM还可以直接在堆上分配对象,保留当前的TLAB不动(至少在线程分配新的对象到TLAB之前保持不变)。假设发生下面这种情况,TLAB的大小为100KB,其中75 KB都已经被占用。这时来了个新的空间分配请求,需要分配30 KB的空间,我们可以回收整个TLAB,这种方式会浪费25 KB的Eden空间。或者直接在堆上分配这个对象,如果下一次的对象分配空间要求小于等于 25KB,线程还可以将 TLAB的空闲空间分配给对象。
JVM提供了各种参数可以控制这些行为,但这一切都取决于TLAB大小。默认情况下,TLAB的大小由三个因素决定:应用程序的线程数。Eden空间的大小和以及线程分配率。由于TLAB的大小计算一定程度上取决于线程分配率。不大可能精准的预测TLAB的大小。
因此两类的应用程序会受益于TLAB参数的调整:需要分配大量巨型对象的应用程序,以及相对于Eden空间的大小而言,应用程序线程数量过多的应用。默认情况下,TLAB就是开启的;使用-Xx: -UseTLAB可以关闭TLAB,不过考虑到TLAB带来的性能提升,关闭这个功能不是个明智的决定。
如何调整TLAB大小?
可以通过-XX:TLABsize=N 来显示的调整TLAB大小(默认是0)。这个标志只能设置TLAB的初始大小,为了避免在每次GC时都调整TLAB的大小,可以使用-XX:-ResizeTLAB标志(大多数的平台上,这个参数的默认值都是true)。这是通过调整TLAB,充分提升对象分配性能最简单的方法(坦率地说,通常这也是最有效的方法)。
一个新的对象无法适配到当前的TLAB中(但是可以容纳于一一个新的、空闲的TLAB中)时,JVM就需要做一些抉择:到底是在堆上分配这个对象,还是要回收当前的TLAB,重新分配-一个新的TLAB来完成这次对象分配,这个决策取决于几个参数。TLAB日志的输出中,refill waste 的值代表了决策的当前阈值:如果TLAB无法容纳新对象的大小超过这个阈值,那么就会在堆上分配新的对象。如果有问题的对象的大小比这个阈值小,就回收老的TLAB空间。
这个值是动态计算得出的,但是默认的初始值是TLAB大小的1%,或者是由参数-XX:TLABWasteTargetPercent=N特别设定的值。每当发生堆上的分配,这个值就增大一笔,增量值由参数-XX:TLABWasteIncrement=N设定(默认值为4)。 这种设计能够避免线程达到TLAB空间占用的阈值,从而持续地在堆上分配对象:随着目标百分比(TargetPercentage)的增大,TLAB空间被回收的几率也在增加。调整TLABWasteTargetPercent参数的结果往往同时伴随着TLAB空间大小的调整,所以,虽然可以调整这个参数,但是效果往往不那么确定。
最终,TLAB空间调整生效时,其容量的最小值可以使用-XX:MinTLABSize=N 参数设置(默认为2 KB)。TLAB空间的最大容量略小于1 GB (使用整型数组可以用到TLAB空间的最大上限,由于对象对齐的原因,最大上限会向下圆整),并且不能修改。