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

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

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

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

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

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

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

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

此外,您可以预先为每个可能的存储桶预留一些空间,而不是维护明确的存储列表。 具体来说,有一个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 

前段时间我写了一篇文章,解释了HotSpot JVM中年轻集合的机制。 了解GMP中的GC暂停,HotSpot的次要GC

脏卡写屏障的原理很简单。 每次程序修改内存中的引用时,都应将修改后的内存页标记为脏。 JVM中有一个特殊的卡表,每个512字节的内存页都与卡表中的一个字节条目相关联。

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

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

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

  • 年轻代(空间):所有新的分配(对象)都进入这个空间。
  • 老一代(空间):这是长寿命对象存在的地方(可能已经死亡)

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

我们使用分代垃圾收集来避免扫描整个存储空间(如Mark和Sweep方法)。 在JVM中,我们有一个小的垃圾收集 ,当GC在年轻一代内部运行时,以及一个主要的垃圾收集(或完整的GC) ,包括年轻和老一代的垃圾收集。

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

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

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

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