个人博客
JVM堆分配中TLAB分配方案 | iwts’s blog
Java对象的内存分配过程如何保证线程安全
对象的内存分配过程中,主要流程是将对象的引用指向一个具体的内存区域,然后进行初始化操作。
但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,可能出现两个线程先后把对象引用指向了同一个内存区域这样的情况。
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是加锁/CAS,本质上都会影响内存的分配效率。
那么在HotSpot
虚拟机中就实现了不涉及同步的解决方案,即TLAB。
TLAB分配方案
Thread Local Allocation Buffer。其主要概念是:
-
每个线程在Java堆中预先分配一小块内存。
-
给对象分配内存的时候,直接在自己这块“私有”内存中分配。
a> 当这部分区域用完之后,再分配新的“私有”内存。
而预先分配的这一小块内存,即TLAB,是从堆的Eden区中划分出来的,但是是某个线程独享的。
在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
TLAB非分配时公有
这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、GC等动作上都是线程共享的。而且在使用上也没有什么区别。
也就是说,虽然每个线程在初始化时都会去堆内存中申请一块TLAB,并不是说这个TLAB区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。
TLAB的GC
在TLAB分配之后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能通过TLAB分配内存,存放在Eden区,但是还是会被GC掉或者被移到Survivor Space、Old Gen等分区中。
大对象在TLAB下的分配
TLAB是在Eden区分配的,但是因为Eden区域本身就不太大,而且TLAB空间的内存也非常小,TLAB在默认情况下仅占有整个Eden空间的1%。
所以,如果是大对象,那么本身是无法在TLAB区直接分配的。
那么这个时候还是可能只在Eden区或者老年代进行分配的,但是这种分配就需要进行同步控制了。
这也是大对象分配比较慢的原因,大对象不能利用TLAB隔离线程,所以只能通过锁等机制保证并发下的同步,所以速度相对小对象的创建要慢不少。
TLAB最大浪费空间
上面也聊过,本来Eden区就小,TLAB更小,那么分配的时候可能经常会碰到扩容的问题。例如:
- 线程的TLAB空间100kb,已经使用了80kb。
- 此时需要分配一个30kb的对象。
- TLAB空间不足。
那么这个时候有两个选择:
-
放弃TLAB,跟大对象一样直接堆内分配。
a> 如果只剩下1kb,那么后续的对象几乎都是在堆内分配,失去了TLAB的意义。
-
放弃当前TLAB,申请一块新的TLAB。
a> TLAB在划分时还是要同步的,如果频繁重新申请TLAB,那么同步操作次数也会多很多,本身性能也会下降。
JVM则提出了最大浪费空间(refill_waste)的概念,用以综合上面两种选择:
当请求分配的内存大于refill_waste的时候,会选择在堆内存中分配。若小于refill_waste值,则会废弃当前TLAB,重新创建TLAB进行对象内存分配。
依旧是上面的例子:
-
TLAB总共100kb,使用了80kb,剩余20kb
-
设置refill_waste=25kb
-
那么有这样的场景:
a> 新对象20kb,恰好分配。
b> 新对象23kb,<25kb,那么放弃TLAB,直接申请一块新TLAB空间并分配
c> 新对象30kb,>25kb,直接走大对象堆分配。