当实现一个停止并复制垃圾收集器时,我需要两个内存库(旧的和新的一个)。一个存储库由汽车和磁带组成。所以基本上当我创建一个新的地址时,它是指向汽车和-cdrs的指针。
分配新内存时,我发现我没有足够的空间,我启动了一个GC。这个人做的是:
现在的问题是:为什么我需要先扫描然后再移动。为什么我不能一起做这两件事?
答案 0 :(得分:5)
听起来你正在通过你实现自己的收藏家的really awesome garbage collection assignment(标记和扫描,停止和复制,世代)。
您的问题的一般答案:通过交换空间时间,双遍算法通常比单程算法使用更少的内存。
更具体的答案:在一个停止和复制收集器中,你通过两次传递:(1)首先将实时数据复制到新的半空间,以及(2)调整实时数据中的内部引用以引用到新半空间中的元素,将旧内存映射到新内存。
您必须意识到执行映射所需的信息并非神奇可用:您需要内存来跟踪如何重定向移动的内存。请记住:您的收藏家本身就是一个程序,它必须使用有界,小的内存量!例如,在收集器中保留哈希表以进行簿记将是禁止的:它不是遵守规则。因此,您需要跟踪的一件事是确保收集器正在播放有限的内存。这就解释了为什么停止和复制收集器将重用旧的半空间并在那里写下那些重定向记录。
考虑到这种限制:重要的是要意识到我们需要小心我们如何遍历现场集。我们选择哪种方法可能会或可能不需要额外的记忆,以一些非常微妙和令人惊讶的方式。特别是,一般情况下的递归不是免费的!从技术上讲,在第一遍中我们应该使用 new 半空间不仅作为我们复制的目标,而且作为我们用来实现的控制堆栈的时髦表示遍历实时数据的递归过程。
具体而言,如果我们像这样做一次通过方法来复制实时集:
;; copy-live-set: number -> void
;; copies the live set starting from memory-location.
Pseudocode:
to copy-live-set starting at memory-location:
copy the block at memory-location over to the new semispace, and
record a redirection record in the old semispace
for each internal-reference in the block:
recursively call copy-live-set at the internal-reference if
it hasn't been copied already
remap the internal-reference to that new memory location
那么你可能会惊讶地发现我们已经弄乱了记忆。上面的内容将打破收集器对运行时环境的承诺,因为这里的递归不是迭代的!它将消耗控制堆栈空间。在实时集遍历期间,它将消耗与我们正在穿过的结构的深度成比例的控制堆栈空间。糟糕!
如果您尝试使用另一种方法来遍历实时集,您应该最终看到有一种很好的方法来遍历整个实时集,同时仍然保证有限的小控制堆栈使用。提示:考虑如何将图形遍历算法编写为一个简单的while循环,并使用一个显式容器来保存接下来要访问的容器,直到我们耗尽容器。如果你恰到好处地斜视,新半空间中的中间值看起来非常像那个容器。
一旦你发现如何遍历常量控制堆栈空间中的实时集,你会发现你需要这两个传递来完成复制和重写内部引用的事情。担心这些细节很麻烦,但是看看垃圾收集者的实际工作方式非常重要。一个真正的收集器需要做这样的事情,关注控制堆栈的使用,以确保它在收集过程中使用有界内存。
总结:双程算法是一种以一段时间为代价帮助我们获取内存的解决方案。但是我们在性能方面付出的代价并不高:虽然我们两次通过实时集,但这个过程在实时集的大小上仍是线性的。
历史:请参阅Cheney's Algorithm,并注意开创性论文的重点标题:“A Nonrecursive List Compacting Algorithm”。单个突出显示的单词“Nonrecursive”是激发双通道方法的关键:它试图避免消耗控制堆栈。切尼的论文是Fenichel和Yochelson“A LISP Garbage-Collector for Virtual-Memory Computer Systems”的论文的延伸,其中作者首先提出了递归的,使用堆栈的方法。为了改善这种情况,Fenichel和Yochelson随后建议使用非递归(但很复杂!)Schorr-Waite DFS algorithm来进行遍历。切尼的方法是一种改进,因为遍历更简单。