CLR 垃圾收集器会主动检查所有已创建的对象,并确定它们是否正在被使用。但是,垃圾收集器如何决定哪些对象将被杀死以及哪些对象正在使用?
我理解为对象分配空值的概念就足够了。但是,如果我只写
string obj = new string(new char[] {'a'});
而不是空赋值行obj = null;
。
垃圾收集器将如何确定何时清理它?
CLR 垃圾收集器会主动检查所有已创建的对象,并确定它们是否正在被使用。但是,垃圾收集器如何决定哪些对象将被杀死以及哪些对象正在使用?
我理解为对象分配空值的概念就足够了。但是,如果我只写
string obj = new string(new char[] {'a'});
而不是空赋值行obj = null;
。
垃圾收集器将如何确定何时清理它?
CLR 垃圾收集器(在其核心)是一个所谓的跟踪GC。(另一个“大”类垃圾收集器是所谓的引用计数GC。)
跟踪 GC 的工作原理是,从一组已知可达的对象中递归地“跟踪”一组可达对象。这是它的工作原理:
假设我们已经有一组我们知道是可达的对象。对于该集合中的每个对象,遵循所有引用(例如字段,以及内部指针,如class
指针等)。这些对象也是可访问的。重复,直到您至少访问过所有对象一次。现在您知道所有可到达的对象。(我们可以说我们已经计算了关于可达性的传递闭包。)你没有访问过的所有对象都是不可访问的,因此有资格进行垃圾回收。
现在,我们只需要弄清楚如何启动这个算法,即如何获得已知可达的第一组对象。好吧,每种语言通常都有一组已知始终可达的对象。我们从中开始跟踪的这个集合称为根集。它包括以下内容:
unsafe
记忆而已。
当然,这个主题有很多变体。这种跟踪思想的最简单实现称为标记扫描。它有两个阶段,标记和扫描(呃!)标记阶段是跟踪阶段,您跟踪可到达的对象,然后在对象标题中设置一点,上面写着“是的,可到达”。在扫描阶段,您收集所有未设置该位的对象并将该位重置为false
其他对象。
这个方案的一个小改进是保留一个单独的标记表。一方面,您不必为了设置那些标记位而在整个 RAM 上进行写入(例如,这会将所有数据从缓存中抛出,并且如果与另一个共享内存,也会触发写时复制过程)。其次,您不必访问可达对象来重置标记位,您可以在完成后扔掉标记表。
这种方案最大的缺点是会导致内存碎片化。最大的优点是对象不会在内存中移动,这意味着例如您可以分发指向对象的指针,而不必担心这些指针可能会变得无效。
另一个非常简单的方案是亨利贝克的半空间复制收集器。它被称为“半空间”,因为它总是使用最多 50% 的分配内存。它也是一个跟踪收集器,但它是一个复制收集器而不是标记扫描。它不是在访问对象时标记对象,而是将它们复制到内存的空白部分。之后,可以简单地在恒定时间内释放旧一半。
这样做的好处是,每次复制对象时,它们都会整齐的紧密地打包在内存中,没有空洞,因此不会产生碎片。但是,它们在内存中移动,因此您不能只分发指向这些对象的指针。
注意:CLR 的垃圾收集器(它实际上有两个!)比我介绍的这两个方案要复杂得多。但是,它们都是跟踪 GC。
第二大类收集器是引用计数收集器。它们不是仅在发生集合时才跟踪引用,而是在每次创建或销毁引用时对引用进行计数。因此,当您将对象分配给局部变量或字段,或将其作为参数传递时,……系统会在对象头中增加一个引用计数器,并且每次您将不同的对象分配给局部变量或局部变量超出范围,或者该字段所属的对象获得 GCd,...,引用递减。如果引用计数为 0,则不再有引用,并且该对象有资格进行垃圾回收。
这种方案的最大优点是您始终可以准确地知道对象何时变得无法访问。最大的缺点是您可能会得到断开连接的循环,其引用计数永远不会为 0。如果您有来自A → B、B → C、C → A和D → B的引用,则A ' s 引用计数为 1,B的引用计数为 2,C的引用计数为 1。如果您现在从D中删除引用,B的引用计数下降到 1,并且系统的其余部分没有对A或B或C的引用,因此它们都无法访问,但它们的引用计数永远不会下降到 0,因此它们永远不会被收集.
GC 的第三个重要思想是世代假设:
事实证明,对于典型系统,几乎所有对象都是如此。这意味着,根据对象的年龄来区别对待对象是有意义的。分代 GC将对象划分为不同的代,并为每一代具有不同的垃圾收集和内存分配策略。(我们就这样吧。)
有关一般垃圾收集的更多信息,您应该阅读Richard Jones、Antony Hosking、Eliot Moss的 The Garbage Collection Handbook – The art of automatic memory management。