首先看一个问题:Java对象的内存分配过程是如何保证线程安全的?
对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作。
但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,在并发场景中,就会存在两个线程先后把对象引用指向了同一个内存区域。如图:
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。所以就有了一个HotSpot虚拟机的解决方案,这种方案被称之为TLAB分配,即
Thread Local Allocation Buffer
。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。TLAB只是HotSpot虚拟机的一个优化方案,不代表所有的虚拟机都有这个特性。
每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。
1. 什么是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区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是经常说的:小的对象比大的对象分配起来更加高效
。
2. 完整的对象分配
通过上面说的可以得出结论,对象分配的全过程如下图:
TLAB的空间并不大,所以大对象还是可能需要在堆内存中直接分配。对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是再eden分配还是在老年代分配。
3. TLAB带来的问题
因为TLAB内存区域并不是很大,所以有可能会经常出现TLAB内存区域不够的情况。在《实战Java虚拟机》中有这样一个例子:
比如一个线程的TLAB空间有100KB,其中已经使用了80KB,当需要再分配一个30KB的对象时,就无法直接在TLAB中分配,遇到这种情况时,有两种处理方案:
- 如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。
- 如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,重新申请TLAB空间再次进行内存分配。
以上两个方案各有利弊,如果采用方案1,那么就可能存在着一种极端情况,就是TLAB只剩下1KB,就会导致后续需要分配的大多数对象都需要在堆内存直接分配。
如果采用方案2,也有可能存在频繁废弃TLAB,频繁申请TLAB的情况,虽然在TLAB上分配内存是线程独享的,但是TLAB内存自己从堆中划分出来的过程确实可能存在冲突的,所以,TLAB的分配过程其实也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。
为了解决这两个方案存在的问题,虚拟机定义了一个
refill_waste
的值,这个值可以翻译为最大浪费空间。
当请求分配的内存大于refill_waste
的时候,会选择在堆内存中分配。若小于refill_waste
值,则会废弃当前TLAB,重新创建TLAB进行对象内存分配。
前面的例子中,TLAB总空间100KB,使用了80KB,剩余20KB,如果设置的refill_waste的值为25KB,那么如果新对象的内存大于25KB,则直接堆内存分配,如果小于25KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间,给新对象分配内存。