JVM 如何确保新对象的内存分配的线程安全

让我们假设这将同时在真正的并行环境(一个 VM)中发生:

// Thread 1:
  new Cat()

// Thread 2:
  new Dog()

// Thread 3:
  new Mouse()

JVM 如何确保堆上内存分配的线程安全?

堆是所有线程的一个,它有自己的内部数据。

为简单起见,假设有一个简单的压缩垃圾回收器实现 ,-XX:+UseSerialGC -XX:+UseParallelGC,使用简单的增量指针来标记可用空间的开始和 Eden 中的一个连续可用空间(堆)。

当为 CatDogMouse 实例分配堆空间时,线程之间必须存在某种同步,否则它们很容易最终相互覆盖。这是否意味着每个操作员都隐藏在一些同步块中?这样,许多“无锁”算法实际上并不是完全无锁;)

我假设内存分配是由应用程序线程本身同步进行的,而不是由另一个专用线程进行的。

我知道 TLAB或线程本地分配缓冲区。它们允许线程在 Eden 中具有单独的内存区域以进行分配,因此不需要同步。但我不确定TLAB是否默认设置,它是一些非常晦涩的HotSpot功能。注意:不要混淆TLAB和变量!ThreadLocal

我还假设,对于更复杂的垃圾回收器,如G1或非压缩垃圾回收器,必须维护更复杂的堆结构数据,例如CMS的可用块列表,因此需要更多的同步。

更新:请让我澄清这一点。我接受HotSpot JVM实现和带有和不具有活动TLAB的变体的答案。

更新:根据我的快速测试TLAB默认设置为打开,在我的64位JDK 7上,用于串行,并行和CMS垃圾收集器,但不适用于G1 GC。


答案 1

在这个答案中,我已经简要描述了HotSpot JVM中的分配过程。
对象的分配方式取决于分配对象的堆区域。

1. 实验室。最快和最常用的方式。

TLAB 是 Eden 中为线程局部分配保留的区域。每个线程可以创建许多 TLAB:一旦一个被填满,就会使用 #2 中描述的技术创建一个新的 TLAB。也就是说,创建新的 TLAB 就像直接在 Eden 中分配一个大型元对象一样。

每个 Java 线程都有两个指针:和 。TLAB 中的分配只是一个指针增量。不需要同步,因为指针是线程本地的。tlab_toptlab_limit

if (tlab_top + object_size <= tlab_limit) {
    new_object_address = tlab_top;
    tlab_top += object_size;
}

-XX:+UseTLAB默认情况下处于启用状态。如果将其关闭,对象将在 Eden 中分配,如下所述。

2.伊甸园(年轻一代)的分配。

如果TLAB中没有足够的空间用于新对象,则可以创建新的TLAB或直接在Eden中分配该对象(取决于TLAB浪费限制和其他人体工程学参数)。

伊甸园中的分配类似于TLAB中的分配。还有两个指针:和 ,它们对于整个 JVM 是全局的。分配也是指针增量,但使用原子操作,因为 Eden 空间在所有线程之间共享。线程安全是通过使用特定于架构的原子指令实现的:CAS(例如 在 x86 上)或 LL/SC(在 ARM 上)。eden_topeden_endLOCK CMPXCHG

3.老一代的分配。

这取决于GC算法,例如CMS使用免费列表。旧版本中的分配通常仅由垃圾回收器本身执行,因此它知道如何同步自己的线程(通常混合使用分配缓冲区,无锁原子操作和互斥锁)。


答案 2

这在 Java 规范中没有指定。这意味着每个JVM都可以按照自己的意愿去做,只要它能工作并遵循Java的内存保证。

关于这如何与移动GC一起工作的一个很好的猜测是,每个线程都有自己的分配区域。在分配对象时,它执行简单指针增加的位置。非常简单,非常快速的分配,没有锁定。当它已满时,它会获得分配给它的新分配区域,或者GC将所有活动对象移动到堆的连续部分,并将现在为空的区域返回给每个线程。我不确定这是否是在任何JVM中实际实现的方式,并且GC同步会很复杂。