在TLAB中创建的对象,如何被其他线程共享?

TLAB本身是Eden区的一部分,但是是线程私有的,启用UseTLAB后,会通过快速对象分配模式在Thread Local Allocation Buffer中进行分配,如果TLAB空间不够时,才会在Eden区其他区域加锁分配。我想问的是,在TLAB中分配的对象——如果需要线程共享访问,hotspot是如何处理这种情况的?我没有看到new指令中实现有判断对象是否是线程私有的逻辑判断,同样在JVM_Clone函数中也没有找到相关逻辑。于是心生疑惑,忘大神解析!膜拜!
关注者
29
被浏览
1119
很简单:在HotSpot VM里,TLAB只有在“分配”这个动作上是线程独占的,而在使用/收集意义上都还是让所有线程共享的;使用上的共享并不需要做任何额外检查。
HotSpot VM目前并没有实现单独收集一个线程的TLAB的GC(这种GC叫做thread-local GC或者缩写为TLGC)。如果实现TLGC,那么TLGC可以看作比minor GC(或者叫Young GC)更轻量的一种GC,只需要暂停当前线程去收集该线程独占的部分的堆,而不需要暂停任何其它Java线程。看似很诱人对不对?请往后面读下去。

所谓TLAB其实就是这样的一个东西:(简化伪代码)
struct ThreadLocalAllocBuffer {
  HeapWord* _start;
  HeapWord* _top;
  HeapWord* _end;
};
每个线程会从Eden分配一大块空间,例如说100KB,作为自己的TLAB。这个start是TLAB的起始地址,end是TLAB的末尾,然后top是当前的分配指针。显然start <= top < end。

在Eden分配空间时,用的是bump-the-pointer方式来分配,但由于Eden是所有Java线程所共享的,在bump pointer的时候必须加锁(或者CAS)才可以保证安全;而当每个线程从Eden分配到一块空间当作TLAB来用之后,在TLAB里分配小块空间同样是bump-the-pointer(上面示意的top指针)则不需要加锁。
当一个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。周而复始。

想像这样的代码:
public class Test {
  public static Test sharedStatic;
  public Test sharedInstanceField;

  public static void foo() {
    Test localVar = new Test();                    // 1
    if (sharedStatic == null) {
      sharedStatic = localVar;                     // 2
    } else {
      sharedStatic.sharedInstanceField = localVar; // 3
    }
  }
}
(这个例子纯粹为了示意“独占”与“共享”的概念,请不要吐槽线程安全问题 >_<)

这里,我们在 (1) 创建了一个新的Test实例。如题主所说,在启动UseTLAB(默认开启)的时候,这个Test实例会被分配在当前执行Test.foo()的线程的TLAB里。TLAB在执行分配动作的时候要更新top指针,而更新这个指针不需要加任何锁。

一个对象能够被多个线程看到其实是一种传递的关系:
  • 基本条件:类的静态变量是肯定被所有Java线程所共享的。如果有静态变量是引用类型的,那么这些引用类型的静态变量所指向的对象也肯定可以被所有Java线程所访问到。
  • 递归条件:一个被共享的引用所指向的Java对象,其中的引用类型字段所指向的Java对象也能被所有Java线程所访问到。
在 (2) 或者 (3) 的地方,我们把指向刚分配出来的Test实例的引用赋值到了一个静态变量或者实例字段上。这种动作就可能导致别的线程可以感知到这个新对象的存在,所以这种动作也叫做“发布”(publish)或者叫做“线程逃逸”(thread escaping)。
执行这样的动作在当前的HotSpot VM中没有针对是否线程逃逸而做任何特别的处理。“共享”是很自然发生的事情。

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