❝很多同学在刚刚学习Java的时候一定有听说过这么一句话:栈管运行,堆管存储。栈空间是每个线程独有一份,不会存在线程安全问题,而堆空间是多线程共享的,需要考虑线程问题。其实在堆空间中也存在着这么一块线程独有的区域,这块区域属于线程独有的,那就是TLAB(Thread Local Allocation Buffer)
❞
TLAB的作用
在解释TLAB之前我们先假设这样一个场景:在并发情况下,多个线程同时向JVM堆空间申请用于存放实例对象的地址,那么此时,同一块堆空间的「地址」是不是会被多个线程抢用呢,答案是肯定的。因此在为对象分配内存空间时会采用「加锁」机制,保证同一块内存地址不会被多个线程同时使用。
一提到加锁,我们的第一反应就是性能效率低下,确实也是如此。为了提高运行效率JVM设计者们就设计了TLAB技术。也就是为每一个线程分配一块线程独有的空间用于存放自己new出来的实例对象。
TLAB区域存在于Eden区,每一个线程所占用的TLAB空间仅为Eden区的1%,可运行一个Java程序后,命令行输入以下命令查看
jinfo -flag TLABWasteTargetPercent 进程id

Java虚拟机给每一个线程分配一段连续的内存空间作为线程私有的TLAB,这个分配的操作是需要加锁的,但是分配完后,各个线程就可以往自己的TLAB中畅通无阻的new对象无需在考虑线程安全了。
看到这里也就明白了对于「内存空间地址」,线程都独有一份TLAB用于new实例对象。但是该地址对于所有线程其实还是可访问读取的,因此我们才一直说堆内存中的对象「数据」是线程共享的。
一个对象在堆中的分配情况
线程需要维护自身TLAB中的两个指针,一个指向TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。接下来的 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。

如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的 TLAB。如果此时该对象占用的空间还是大于新分配的TLAB那么直接加锁在Eden区中生成。

以上就是大体的对象内存分配流程。接着我们再细化一下:
编译器通过逃逸分析确定对象是否可以通过标量替换,如果可以就直接在栈上分配 如果逃逸分析后发现必须在堆中分配,则首先在该线程的TLAB上分配 判断TLAB中【top】+【对象size】<=【end】。如果满足条件则该对象在TLAB分配成功 如果判断失败则重新申请新的 TLAB。然后再进行【top】+【对象size】<=【end】判断。如果满足条件则在新的TLAB分配即可 如果发现还是放不下就直接加锁在Eden区进行分配。 如果发现在Eden区还是放不下就触发一次YGC,清空整个Eden区后再次为该对象分配空间。 如果清空后的Eden区还是放不下就直接将对象放入老年代区
总结
通过以上理解,针对“堆是线程共享的”这句话其实准确来说是:堆的数据是所有线程共享的。但是JVM为了提高内存分配的效率,在堆中为每一个线程划分了一块线程私有的内存地址空间TLAB。通过TLAB线程不需要每次在进行new对象时对堆空间进行加锁,因此也就增加了系统的并发性。




