栈上分配、TLAB

JAVA对象分配流程

如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代,如下图。

这里写图片描述

栈上分配

栈上分配是java虚拟机提供的一种优化技术,基本思想是对于那些线程私有的对象(指的是不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提供系统的性能。

栈上分配的一个技术基础是进行逃逸分析

逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。在运行时分析对象的生命周期,如果发现该对象只会被本线程使用(一般是一些局部对象),那么就将该对象在栈上分配,而不在堆中(heap)分配,以减少对象对堆的压力,减少GC的次数。

TLAB(Thread Local Allocation Buffer)

即线程本地分配缓存区,这是一个线程专用的内存分配区域。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

每个线程会从Eden分配一大块空间,例如说100KB,作为自己的TLAB。这个start是TLAB的起始地址,end是TLAB的末尾,然后top是当前的分配指针。显然start <= top < end。

当一个Java线程在自己的TLAB中分配到尽头之后,再要分配就会出发一次“TLAB refill”,也就是说之前自己的TLAB就“不管了”(所有权交回给共享的Eden),然后重新从Eden里分配一块空间作为新的TLAB。所谓“不管了”并不是说就让旧TLAB里的对象直接死掉,而是把那块空间的控制权归还给普通的Eden,里面的对象该怎样还是怎样。通常情况下,在TLAB中分配多次才会填满TLAB、触发TLAB refill,这样使用TLAB分配就比直接从共享部分的Eden分配要均摊(amortized)了同步开销,于是提高了性能。其实很多关注多线程性能的malloc库实现也会使用类似的做法,例如TCMalloc。

到触发GC的时候,无论是minor GC还是full GC,要收集Eden的时候里面的空间无论是属于某个线程的TLAB还是不属于任何TLAB都一视同仁,把Eden当作一个整体来收集里面的对象——把活的对象拷贝到survivor space(或者直接晋升到Old Gen)。在GC结束之后,每个Java线程又会重新从Eden分配自己的TLAB。周而复始。

线程逃逸:
我们把指向刚分配出来的Test实例的引用赋值到了一个静态变量或者可以被其他线程访问的实例字段上时,就可能导致别的线程可以感知到这个新对象的存在,所以这种动作也叫做“发布”(publish)或者叫做“线程逃逸”(thread escaping)

如果HotSpot VM要实现前面提到的TLGC的话,那就必须要在线程逃逸发生的时候做一些特殊处理了。
所谓特殊处理可以是在发生线程逃逸时触发一次minor GC来把当前TLAB里有被共享变量所引用的对象移动到Eden的共享部分去,这种动作叫做“全局化”(globalization)。也可以有别的做法,例如说在发生线程逃逸时先做些标记而不立即触发全局化,想办法把全局化GC推迟一点做,这样可以更高效一些。全局化GC跟普通的minor GC开销差不多,如果一个线程在期望的触发正常TLGC之前触发了一次或多次全局化GC的话,做TLGC就得不偿失了。正是因为如何高效处理全局化是个很麻烦、需要非常细致地处理的事情,所以HotSpot VM才迟迟没有把这个功能做到主干版本上。

扫描二维码关注公众号,回复: 2331733 查看本文章

补充问题:TLAB分配的对象可以共享吗?
答:只要是Heap上的对象,所有线程都是可以共享的,就看你有没有本事访问到了。在GC的时候只从root sets来扫描对象,而不管你到底在哪个TLAB中。

参考资料:
https://www.zhihu.com/question/56538259
https://blog.csdn.net/xiaomingdetianxia/article/details/77688945

猜你喜欢

转载自blog.csdn.net/yangsnow_rain_wind/article/details/80434323