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

Daw*_*wid 23 java garbage-collection

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

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

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

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

小智 28

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

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

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

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

总之,当写入障碍发现该语句some_obj.field = other_obj;将旧指针存储在旧对象中时,它执行以下操作:

card_table[(&old_obj - start_of_heap) >> K] = 1;
Run Code Online (Sandbox Code Playgroud)

&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
Run Code Online (Sandbox Code Playgroud)


Ale*_*zin 14

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

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

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


Ham*_*eji 6

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

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

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

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

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

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

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

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

现在,当进行少量垃圾回收时,首先要遵循从活动根到年轻对象的所有引用,并将年轻代中的引用对象标记为活动。然后,不扫描所有旧对象来查找对年轻对象的引用,而是扫描卡表。如果GC在卡表中找到任何标记的区域,它将加载相应的对象,并遵循其对年轻对象的引用并将其标记为活动。