【问题标题】:How JVM ensures thread safety of memory allocation for a new objectJVM 如何确保新对象内存分配的线程安全
【发布时间】:2014-10-20 03:39:12
【问题描述】:

让我们假设这将在一个真正的并行环境中同时发生,一个虚拟机:

// Thread 1:
  new Cat()

// Thread 2:
  new Dog()

// Thread 3:
  new Mouse()

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

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

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

当为 CatDogMouse 实例分配堆空间时,线程之间必须存在某种同步,否则它们很容易结束向上覆盖彼此。这是否意味着每个 new 运算符都隐藏在一些同步块中?这样一来,许多“无锁”算法实际上并不是完全无锁的;)

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

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

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

更新:请让我澄清一下。我接受 HotSpot JVM 实现和变体的答案,无论是否有活动的 TLAB。

更新: 根据my quick testTLAB 默认设置为 ON,在我的 64 位 JDK 7 上,用于串行、并行和 CMS 垃圾收集器,但不适用于 G1 GC。

【问题讨论】:

  • 就是这样。给每个线程自己的“区域”或“池”是一种方法。但是某种锁定协议通常会用于从单个池中分配的大型对象。

标签: java multithreading concurrency jvm new-operator


【解决方案1】:

我在this answer中简要描述了HotSpot JVM中的分配过程。
分配对象的方式取决于分配对象的堆区域。

1。实验室。最快最频繁的方式。

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

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

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

-XX:+UseTLAB 默认启用。如果关闭它,对象将在 Eden 中分配,如下所述。

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

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

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

3。在老年代分配。

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

【讨论】:

  • 好的,看起来像个赢家。还有一件事,如果 TLAB 不活动,对象会直接在 Eden 上分配,如第 2 节所述?请问你能更新你的答案吗?谢谢。
  • 还有逃逸分析导致堆栈分配。
  • @DavidEhrmann 在 HotSpot 中没有“堆栈分配”之类的东西。我在other answer 中对此进行了描述。
  • @apangin 您正在为将来可能会改变的实现细节分心。
  • @apangin 如果 2 个不同的线程碰巧在堆中分配相同的大小,为什么 CAS 可以确保线程安全?
【解决方案2】:

这在 Java 规范中没有指定。这意味着每个 JVM 都可以随心所欲地执行它,只要它可以工作并遵循 Java 的内存保证。

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

【讨论】:

  • 非常不可能。几乎可以肯定有一个同步步骤。
  • @EJP,Smith_61 提议的内容中一个同步步骤。线程每次用完当前分配区域时都必须锁定互斥锁。 (注意:我并没有声称知道 Oracle 的 JVM 或任何其他 JVM 是如何实际进行分配的。)
  • @EJP - 通过使用区域/池,系统只需要在需要刷新区域/池时进行同步。我可以向你保证,这就是在某些 JVM 中的做法。
  • 呃 - 所以向另一个线程发送一个 10MB 的对象意味着复制它?
  • @MartinJames - 无需复制对象以将其“信号”到另一个线程。对象的分配位置/方式无关紧要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-04-30
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 2021-12-30
相关资源
最近更新 更多