在垃圾回收中使用哈希表是否可以解决标记和清除问题?


13

在mark-sweep-compact垃圾回收算法中,重新定位对象时必须停止工作,因为引用图变得不一致,并且必须替换指向该对象的所有引用的值。

但是,如果您有一个哈希表,其中对象ID为键,指针为值,并且引用将指向该ID而不是对象地址...那么固定引用将仅需要更改一个值,并且仅当对象时才需要暂停试图在复制期间写入...

我的思路有误吗?

Answers:


19

更新引用不是唯一需要暂停的操作。通常归类为“标记清除”的标准算法都假定整个对象图在被标记时保持不变。正确处理修改(创建新对象,更改引用)需要相当棘手的替代算法,例如三色算法。总称是“并发垃圾收集”。

但是,是的,压缩后更新引用也需要暂停。是的,使用间接寻址(例如,通过持久对象ID和到实际指针的哈希表)可以大大减少暂停。如果有需要,甚至可以使该部件无锁。与任何低级共享内存并发一样,正确的选择仍然很棘手,但是没有根本的理由认为它不起作用。

但是,这将具有严重的缺点。除了占用额外的空间(所有对象至少要有两个额外的单词)之外,它还会使每次取消引用的开销都大大增加。现在,甚至像获取属性一样简单的事情都涉及到完整的哈希表搜索。我估计性能影响会比增量跟踪更差。


好吧,我们今天有很多内存,所以我们可以说50 Mb表和哈希可以是简单的模,因此只有一条指令...
mrpyo 2014年

3
@mrpyo获取哈希表的大小,进行模运算,从哈希表偏移量取消引用以获取实际的对象指针,从而取消引用对象本身。再加上一些寄存器改组。我们最终得到4条以上的指令。此外,此方案还存在有关内存局部性的问题:现在,哈希表和数据本身都必须放入缓存中。
阿蒙2014年

@mrpyo每个对象需要一个条目(对象ID->当前地址),对吗?而且,无论哈希函数的价格多么便宜,您都会遇到冲突,需要解决它们。阿蒙也说了什么。

@amon这只是一个CPU的前一个时间的问题有50MB以上的高速缓存:)
莫兹

1
@Ӎσᶎ到那时,我们可以在芯片上放置50 MiB的晶体管,并且仍然具有足够低的延迟,使其可以用作L1或L2缓存(L3缓存的大小已经达到15 MiB,但通常为片外AFAIK,而且延迟比L1和L2差),因此,我们将拥有大量的主内存(以及要放入的数据)。表不能固定大小,它必须随堆一起增长。

19

计算机科学中的所有问题都可以通过另一种间接解决方案来解决……除了太多的间接设计问题

您的方法不能立即解决垃圾收集问题,而只能将其上移一个级别。而且要花多少钱!现在,每个内存访问都通过另一个指针取消引用。我们无法缓存结果位置,因为它可能已同时被重定位了,所以我们必须始终通过对象ID。在大多数系统中,这种间接方式是不可接受的,并且假设停止世界运行总成本较低。

我说你的主张只会解决问题,不会解决问题。问题在于对象ID的重用。现在,对象ID相当于我们的指针,并且只有有限数量的地址。可以想象到(尤其是在32位系统上),在程序的生命周期内,将创建多个INT_MAX对象,例如在类似以下的循环中

while (true) {
    Object garbage = new Object();
}

如果仅增加每个对象的对象ID,则在某些时候我们将用完ID。因此,我们必须找出哪些ID仍在使用中,哪些ID是免费的,以便可以对其进行回收。听起来有点熟?我们现在回到了第一广场。


大概可以使用256位数字表示“足够大”的ID吗?我并不是说这个想法总体上是好的,但是您几乎可以肯定可以重用IDS。

@Vality实际上是可以的–就我们所知,这可以解决ID重用的问题。但这只是另一个“ 640K对任何人都应该足够”的说法,实际上并不能解决问题。更具灾难性的方面是,必须增加所有对象(和哈希表)的大小,以容纳这些过大的伪指针,并且在哈希访问期间,我们需要将此bigint与其他ID进行比较,这可能会占用多个寄存器,并接受多条指令来完成操作(在64位上:8倍负载,4倍比较,3倍,这比本地int增加5倍)。
2014年

是的,一段时间后您将用完ID,并且需要更改所有ID,这需要暂停。但这可能是罕见的事件……
mrpyo 2014年

@amon非常同意,那里的所有优点都很好,拥有一个我真正认同的可持续发展体系要好得多。无论您怎么做,这都将是令人难以忍受的缓慢,无论如何这只是理论上的有趣。无论如何,我个人并不是垃圾收集者的忠实粉丝:P
Vality

@amon:世界上有比这更多的代码,一旦包装了一个64位ID(584年纳秒),就会出错,并且您可能可以安排内存分配占用1ns,尤其是如果您不对全局计数器进行分片的话会吐出ID!)。但是可以肯定的是,如果您不需要依赖于此,那么您就不需要了。
史蒂夫·杰索普

12

您的思路没有错误,您刚刚描述了与原始Java垃圾收集器的工作原理非常接近的内容

原始Java虚拟机[6]和某些Smalltalk虚拟机使用间接指针(在[6]中称为句柄)来引用对象。通过使用句柄,句柄允许在垃圾回收期间轻松地重定位对象,因为只有句柄直接指向每个对象:句柄中的一个。通过该对象间接引用该对象的所有其他引用。在这种基于句柄的存储系统中,尽管对象地址在对象的生存期内发生变化,因此无法用于哈希处理,但句柄地址保持不变。

垃圾收集对象的时空散列

在Sun当前对Java虚拟机的实现中,对类实例的引用是指向本身就是一对指针的句柄的指针:一个指向包含对象方法的表的指针,以及指向代表对象的Class对象的指针。对象的类型,另一个分配给从Java堆为对象数据分配的内存。

Java虚拟机规范(1997)

因此,它确实起作用,已经进行了尝试,其效率低下导致了世代标记和清除系统的发展。


大概这些句柄不是哈希表中的键(如问题所示)吗?不需要,只是一个包含指针的结构。然后,句柄的大小都相同,因此可以从堆分配器中分配它们。从本质上讲,它不需要内部压缩,因为它不会分散。您可能会哀悼该分配器使用的大块无法重新放置。可以通过其他间接级别解决;-)
史蒂夫·杰索普

@SteveJessop是的,在gc实现中没有哈希表,尽管句柄的值也是由Object.getHashCode()
Pete Kirkham
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.