垃圾收集器命令

Garbarge collector order

我知道 .Net 垃圾收集器在销毁对象时不能保证任何顺序,但我对以下行为感到非常惊讶:

using System;
using System.Collections.Generic;

namespace ConsoleApplication2 {
class Program {

    class A {
        public A() { i = 1; }
        private int i;

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        ~A() {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Console.WriteLine("Disposed A");
        }
    }

    class B {
        static int p = 0;
        public B(A a) {
            this.a = a;
            j = p++;
        }

        ~B() {
            Console.WriteLine("Disposed " + j.ToString());
        }

        private int j;
        private A a;
    }

    static void Main(string[] args) {
        A a_obj = new A();
        List<B> bs = new List<B>();
        for (int i = 0; i < 10; i++) {
            B b = new B(a_obj);
            bs.Add(b);
        }
    }
}
}

这次执行有以下(可能的)输出:

处置9

已处置 2

已处理 1

已处置 0

处置A

处置8

处置 7

处置 6

处置 5

处置 4

处置 3

我能理解 B 对象的顺序不是确定的,而是 如果仍然有一些 B 对象引用了 a_obj 并且还没有被销毁,那么垃圾收集器怎么可能销毁 a_obj?

如果引用 a_obj 的元素中的 none 个被引用,GC 可以收集它,因为应用程序根无法再次获得对它的引用。

这是a good tutorial on C# object lifecycle

的相关部分

在垃圾回收过程中,运行时将调查托管堆上的对象以确定应用程序是否仍可访问(根)它们。为此,CLR 将构建一个对象图,它代表堆上的每个可达对象。对象图用于记录所有可达的对象。 [...] 构建图后,无法访问的对象将被标记为垃圾。

输出是在进程关闭时产生的,此时GC正在清理进程中分配的整个内存,对象之间的引用不影响清理顺序。

.NET 垃圾收集器不使用引用计数,因此对 a_obj 的引用有多少并不重要。重要的是从根可以访问哪些对象(如静态变量)。在您的情况下,所有对象(A 和所有 Bs)同时变得无法从 root 访问,因此都符合垃圾收集的条件,它们的相互引用无关紧要。如果 AB 都不可访问并且将在同一个循环中被收集,那么为什么还要考虑发生这种情况的顺序呢?此外,如果顺序取决于哪些对象引用了哪些 - 考虑一下它将如何收集相互引用的对象?

这里也引用了 documentation 中的一句话,完全符合您的情况:

The finalizers of two objects are not guaranteed to run in any specific order, even if one object refers to the other. That is, if Object A has a reference to Object B and both have finalizers, Object B might have already been finalized when the finalizer of Object A starts.

另请注意,首先所有的终结器都会 运行,然后对象才会真正死亡,并且它们占用的内存可能会被重用(您甚至可以通过 GC.ReRegisterForFinalize ressurect 终结器中的对象)。因此您可以从 B 的终结器访问 A,即使此时 A 的终结器可能已经是 运行。显然你应该避免这样做。