垃圾收集器如何防止每次收集都扫描整个内存?


16

一些(至少是Mono和.NET的)垃圾收集器具有它们经常扫描的短期存储区域,以及具有次要扫描频率的辅助存储区域。Mono将其称为托儿所。

为了找出可以丢弃的对象,它们扫描从根,堆栈和寄存器开始的所有对象,并处置所有不再被引用的对象。

我的问题是,它们如何防止所有使用中的内存在每次收集时都被扫描?原则上,找出不再使用哪些对象的唯一方法是扫描所有对象及其所有引用。但是,这将防止OS换出内存,即使应用程序未使用它,也感觉需要进行大量工作,对于“ Nursery Collection”也是如此。感觉他们并没有通过使用托儿所而获得太多收益。

我是否缺少某些东西?或者垃圾收集器实际上是在每次收集对象时扫描每个对象和每个引用吗?


1
Angelika Langer撰写的《垃圾收集的艺术调整》一书对此做了很好的概述。在形式上,它是关于它是如何在Java中所做的那样,但是呈现的概念是相当多的语言无关
蚊蚋

Answers:


14

允许世代垃圾收集避免扫描所有较早对象的基本观察结果是:

  1. 收集之后,所有仍存在的对象将达到最小生成量(例如,在.net中,Gen0收集之后,所有对象都是Gen1或Gen2; Gen1或Gen2收集之后,所有对象都是Gen2)。
  2. 自从将所有事物提升到N代或更高世代的集合以来未写过的对象或其一部分,不能包含对低代对象的任何引用。
  3. 如果某个对象已达到某个世代,则无需将其标识为可到达以确保在收集较低世代时保留它。

在许多GC框架中,垃圾回收器可能以这样的方式标记对象或其部分:首次写入对象的尝试将触发特殊代码,以记录已被修改的事实。无论对象是什么,已修改的对象或其部分,都必须在下一个集合中进行扫描,因为它可能包含对较新对象的引用。另一方面,有很多旧的对象在集合之间没有被修改的情况很常见。低代扫描可以忽略此类对象的事实可以使此类扫描比其他方式更快地完成。

顺便说一句,即使人们无法检测到何时修改了对象,并且必须在每次GC遍历中扫描所有内容,但分代垃圾回收仍可以提高压缩收集器的“清理”阶段性能。在某些嵌入式环境中(尤其是在顺序和随机存储器访问之间的速度差异很小或没有差异的环境中),与标记引用相比,移动存储器块相对昂贵。因此,即使无法使用分代收集器加速“标记”阶段,加快“扫描”阶段可能还是值得的。


在任何系统中移动内存块都是昂贵的,因此即使在Quad Ghz CPU系统上,提高扫描性能也是一项收益。
gbjbaanb 2012年

@gbjbaanb:在许多情况下,即使完全免费地移动物体,扫描所有物体以寻找活动物体的成本也是巨大且令人反感的。因此,在实际操作中应避免扫描旧物体。另一方面,避免压缩较旧的对象是一个简单的优化,即使在简单的框架上也可以实现。顺便说一句,如果有人正在为小型嵌入式系统设计GC框架,则对不可变对象的声明式支持可能会有所帮助。跟踪可变对象是否已更改很困难,但是可能会做得很好……
supercat 2012年

...简单地假设每一次GC通过都需要扫描可变对象,而不变对象则不需要。即使构造不可变对象的唯一方法是在可变空间中构造“原型”然后进行复制,单个额外的复制操作也可以避免在将来的GC操作中扫描该对象。
supercat 2012年

顺便说一句,在某些情况下,如果程序生成的字符串永远不变,则复制“下一个字符串分配”指针指向“字符串空间顶部”指针。这种更改将阻止垃圾收集器检查任何旧字符串以查看是否仍然需要它们。Commodore 64几乎不是高科技,但是这样的“世代” GC甚至也可以提供帮助。
2012年

7

您所指的GC是世代垃圾收集器。它们经过精心设计,可以最大程度地利用“婴儿死亡率”或“世代假设”这一观察结果,这意味着大多数对象很快就会变得无法触及。它们确实从根开始扫描,但是忽略所有旧对象。因此,它们不需要扫描内存中的大多数对象,而只扫描年轻的对象(以不检测到无法访问的旧对象为代价,至少在那时不这样做)。

我听到你尖叫:“但是那是错的。旧物体可以而且确实是指年轻物体”。您说对了,有多种解决方案,所有解决方案都围绕快速有效地获取知识,必须检查哪些旧对象以及哪些可以忽略不计。它们几乎可以归结为记录对象或较小的(比对象大,但比整个堆小)的内存范围,其中包含指向年轻一代的指针。其他人比我描述的要好得多,所以我只给您几个关键词:卡片标记,记住的设置,写障碍。还有其他技术(包括混合技术),但是这些技术涵盖了我所知道的常见方法。


3

要找出仍然存在的苗圃对象,收集器仅需要扫描根集和自上次收集以来已发生变异的任何旧对象,因为最近未发生变异的旧对象不可能指向年轻的对象。有多种算法可以以不同的精度级别(从准确的一组突变字段到一组可能发生突变的页面集)维护此信息,但是它们通常都涉及某种写障碍:在每个引用上运行的代码类型的字段突变,可更新GC的簿记。


1

最古老,最简单的垃圾收集器实际上确实扫描了所有内存,并且在它们执行时必须停止所有其他处理。后来的算法以各种方式对此进行了改进-使复印/扫描递增,或并行运行。大多数现代垃圾收集器将对象分成几代,并仔细管理跨代指针,以便可以收集新一代而不破坏旧的指针。

关键点在于,垃圾收集器与编译器以及其余的运行时紧密协作,以保持其正在监视所有内存的错觉。


我不确定在1970年代末之前在小型计算机和大型机中使用过哪种垃圾收集方法,但是Microsoft BASIC垃圾收集器(至少在6502计算机上)会将其“下一个字符串”指针设置为内存顶部,然后进行搜索所有字符串引用以查找位于“下一个字符串指针”下方的最高地址。该字符串将被复制到“下一个字符串指针”的正下方,而该指针将停在其正下方。然后算法将重复。代码可能会纠结指针以提供...
supercat 2012年

...类似世代相传的收藏 我有时想知道修补BASIC来实现“世代”集合的难度有多大,方法是简单地保留每一代顶部的地址,并在每个GC周期之前和之后添加一些指针交换操作。GC性能仍然会很差,但是在许多情况下可能会从几十秒缩短到十分之一秒。
2012年

-2

基本上... GC使用“存储桶”来区分正在使用的内容和未使用的内容。一旦进行检查,它将清除未使用的内容,并将其他所有内容移至第二代(其频率低于第一代),然后将仍在使用的内容移至第二代。

因此,第三代中的对象通常是由于某种原因而被打开的对象,GC并不经常检查那里。


1
但是,它如何知道正在使用哪些对象?
Pieter van Ginkel,2012年

它跟踪可访问代码中可访问的对象。一旦对象不再从任何代码到达能执行(比如,对于已经返回方法的代码),那么GC知道它是安全的收集
JohnL

你们两个都在描述GC如何正确,而不是效率如何。从问题来看,OP非常了解这一点。

@delnan是的,我正在回答一个问题,即它如何知道正在使用哪些对象,这就是Pieter的评论。
JohnL 2012年

-5

此GC通常使用的算法是朴素的标记扫频

您还应该知道,这不是由C#本身管理,而是由所谓的CLR管理


这是我从阅读有关Mono的垃圾收集器时得到的感觉。但是,我不明白的是,如果他们要扫描有史以来收集的完整工作集,为什么他们会有一个世代收集器,而GEN-0收集器可以非常快速地收集他。假设有2GB的工作集,怎么能这么快?
Pieter van Ginkel,2012年

那么,对于单真正的GC是SGEN,你应该阅读这mono-project.com/Generational_GC或者一些网上的文章schani.wordpress.com/tag/mono infoq.com/news/2011/01/SGen,问题是,诸如CLR和CLI这样的新技术具有真正的模块化设计,该语言仅成为表达CLR的一种方式,而不是生成二进制代码的方式。您的问题是关于实现的细节,而不是算法,因为算法仍然没有实现,您应该只阅读Mono的技术论文和文章。
user827992 2012年

我很困惑。垃圾收集器使用的策略不是算法吗?
Pieter van Ginkel,2012年

2
-1停止混淆OP。GC是CLR的一部分,而不是特定于语言的,这一点都不重要。GC的主要特点是它对堆进行布局并确定可达性的方式,而后者全部与用于此的算法有关。虽然算法可以有很多实现,并且您不应该陷入实现细节,但仅算法就确定要扫描多少个对象。世代GC只是一种算法+堆布局,它试图利用“世代假设”(大多数对象都年轻了)。这些都不是天真的。

4
实际上是算法!=实现,但是实现只能在变成另一种算法的实现之前偏离很远。在GC领域,算法描述非常具体,其中包括不扫描苗圃收集中的整个堆以及如何查找和存储代间指针的事情。确实,算法不会告诉您算法的特定步骤将花费多长时间,但这与该问题根本无关。
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.