实际卡表和作家屏障如何工作?

时间:2013-10-03 08:41:58

标签: java garbage-collection

我正在阅读一些关于Java中垃圾收集的资料,以便更深入地了解GC过程中真正发生的事情。

我遇到了名为“卡表”的机制。我用Google搜索并没有找到全面的信息。大多数解释都很浅,并且描述它就像一些魔术。

我的问题是:卡表和写屏障如何工作?卡表中标有什么?然后垃圾收集器如何知道特定对象被老一代持久存在的另一个对象引用。

我想对这种机制有一些实际想象,就像我应该准备一些模拟一样。

3 个答案:

答案 0 :(得分:27)

我不知道你是否发现了一些特别糟糕的描述,或者你是否期望有太多细节,我对explanations I've seen非常满意。如果描述简短且声音简单,那是因为它确实是一个相当简单的机制。

正如您显然已经知道的那样,分代垃圾收集器需要能够枚举引用年轻对象的旧对象。扫描所有旧对象是正确的,但这会破坏代际方法的优势,因此您必须缩小范围。无论你如何做到这一点,你都需要一个写屏障 - 只要一个成员变量(引用类型)被分配/写入,就会执行一段代码。如果新引用指向一个年轻的对象并且它存储在一个旧对象中,则写入障碍会记录该垃圾收集的事实。不同之处在于它的记录方式。有一些确切的方案使用所谓的记忆集,每个旧对象的集合,它具有(在某些时候)对年轻对象的引用。你可以想象,这需要相当多的空间。

卡表是一种权衡:它不是告诉你哪些对象完全包含年轻指针(或至少在某些时候做过),它将对象分组为固定大小的桶和轨道存储桶包含带有年轻指针的对象。当然,这可以减少空间使用。为了正确起见,只要你对它保持一致,它对你如何铲斗对象并不重要。为了提高效率,你只需按照它们的内存地址对它们进行分组(因为你可以免费获得),除以两个更大的幂(为了使除法成为一个便宜的按位运算)。

此外,您可以预先为每个可能的存储桶预留一些空间,而不是维护显式的存储列表。具体来说,有一个N位或字节的数组,其中N是桶的数量,因此如果i桶不包含年轻指针,则i值为0,如果不包含年轻指针则为1确实包含年轻指针。这是适当的卡桌。通常,这个空间被分配和释放,同时还有一大块内存用作堆的一部分。它甚至可以嵌入在内存块的开头,如果它不需要增长。除非将整个地址空间用作堆(这是非常罕见的),否则上面的公式给出从start_of_memory_region >> K而不是0开始的数字,因此要获得卡表的索引,您必须减去开始的开始堆的地址。

总之,当写屏障发现语句some_obj.field = other_obj;将年轻指针存储在旧对象中时,它会这样做:

card_table[(&old_obj - start_of_heap) >> K] = 1;

其中&old_obj是现在有一个年轻指针的对象的地址(由于它刚刚确定引用旧对象,因此已经在寄存器中)。 在次要GC期间,垃圾收集器查看卡表以确定要扫描年轻指针的堆区域:

for i from 0 to (heap_size >> K):
    if card_table[i]:
        scan heap[i << K .. (i + 1) << K] for young pointers

答案 1 :(得分:12)

前段时间我写了一篇文章,解释了HotSpot JVM中年轻收藏的机制。 Understanding GC pauses in JVM, HotSpot's minor GC

  

脏卡写入障碍的原理非常简单。每次程序修改内存中的引用时,都应将修改后的内存页标记为脏。 JVM中有一个特殊的卡表,每个512字节的内存页在卡表中都有一个字节条目。

     

通常从旧空间到年轻人的所有参考文献的收集都需要扫描旧空间中的所有对象。这就是我们需要写屏障的原因。自从上次重置写屏障以来,年轻空间中的所有对象都已创建(或重新定位),因此非脏页不能引用到年轻空间。这意味着我们只能扫描脏页中的对象。

答案 2 :(得分:3)

对于正在寻找简单答案的任何人:

在JVM中,对象的内存空间分为两个空间:

  • 年轻一代(空间):所有新分配(对象)都进入该空间。
  • 上一代(空间):这是存在寿命很长的物体(可能会死亡)的地方

这个想法是,一旦一个对象幸存了几个垃圾回收,它就更有可能长期生存。因此,在垃圾回收中存活超过一个阈值的对象将被提升为老一代。垃圾收集器在年轻一代中运行频率更高,而在老一代中运行频率更低。这是因为大多数对象的生存时间都很短。

我们使用分代垃圾回收来避免扫描整个内存空间(例如Mark and Sweep方法)。在JVM中,我们有一个次要垃圾回收(这是GC在年轻一代中运行的时间)和一个主要垃圾回收(或完整的GC),其中包含了新旧垃圾回收。几代人。

在进行次要垃圾收集时,我们会遵循从活动根到年轻一代对象的所有引用,并将这些对象标记为活动对象,这会将它们从垃圾收集过程中排除。问题是,从旧世代的对象到年轻世代的对象可能会有一些引用,GC应该考虑这些引用,这意味着由旧世代的对象引用的年轻世代的对象也应标记为活动对象并从垃圾收集过程中排除。

解决此问题的一种方法是扫描旧一代中的所有对象,并找到它们对年轻对象的引用。但是这种方法与分代垃圾收集器的思想相矛盾。 (为什么我们首先将记忆分解成几代人?)

另一种方法是使用写障碍和卡表。当老一代的对象写入/更新对年轻一代的对象的引用时,此操作将经历称为写障碍的操作。当JVM看到这些写障碍时,它将更新卡表中的相应条目。卡表是一个表,它的每个条目对应于512字节的内存。您可以将其视为包含01项的数组。 1项表示在内存的相应区域中有一个对象,其中包含对年轻一代对象的引用。

现在,当发生较小的垃圾收集时,首先会跟踪从活动根到年轻对象的每个引用,并将年轻代中的引用对象标记为活动。然后,不扫描所有旧对象来查找对年轻对象的引用,而是扫描卡表。如果GC在卡表中找到任何标记的区域,它将加载相应的对象,并遵循其对年轻对象的引用并将其标记为活动。