循环中不可访问对象的垃圾收集

本文关键字:对象 访问 循环 | 更新日期: 2023-09-27 18:08:16

如果我有一个这样的循环:

public class Foo {
     public Foo Foo;
     public Foo() {
     }
}
class Program {
    public static void Main(string[] args) {
         var foo = new Foo();
         long i = 0;
         while(i < Int64.MaxValue) {
             foo.Foo = new Foo();
             foo = foo.Foo;
             if(i % 10000 == 0)
                 GC.Collect();
             i++;
         }
         GC.Collect();
    }
}

垃圾收集器在退出循环之前不会清理父对象。为什么呢?一旦foo被重新分配,我看不到任何从代码中引用它们的方法,所以它们不应该被清理吗?

我正在查看任务管理器中进程的内存使用情况,通过了我设置的一些断点来确定正在发生这种情况。它在循环内继续上升(如果我将其设为无限,则会上升到多个gb),但在循环退出并调用第二个GC.Collect()时立即下降。

循环中不可访问对象的垃圾收集

下面是稍微修改过的程序,更清楚地演示了这种行为:

class Foo
{
    public int Value;
    public Foo Next;
    public Foo(int value) { this.Value = value; Console.WriteLine("Created " + this.Value); }
    ~Foo() { Console.WriteLine("Finalized " + this.Value); }
}
class Program
{
    public static void Main(string[] args)
    {
        var foo = new Foo(0);
        for (int value = 1; value < 50; ++value)
        {
            foo.Next = new Foo(value);
            foo = foo.Next;
            if (value % 10 == 0)
            {
                Console.WriteLine("Collecting...");
                GC.Collect();
                Thread.Sleep(10);
            }
        }
        Console.WriteLine("Exiting");
    }
}

在。net 4.5上,当我在调试模式下构建目标任何CPU或x86时,我重现了您所看到的行为:直到"退出"打印之后实例才最终确定。但是,当我在发布模式目标x64(即使在调试模式下构建)下构建时,实例一旦无法访问就会最终确定:

Created 0
Created 1
Created 2
Created 3
Created 4
Created 5
Created 6
Created 7
Created 8
Created 9
Created 10
Collecting...
Finalized 9
Finalized 0
Finalized 8
Finalized 7
Finalized 6
Finalized 5
Finalized 4
Finalized 3
Finalized 2
Finalized 1
Created 11
Created 12
Created 13
...

为什么会发生这种情况?我想只有CLR专家才能确切地告诉我们,但我的猜测是:行为取决于JIT编译器和优化器碰巧生成的机器码的特定细节,这些细节根据目标指令集和您是否在调试模式下运行而有所不同。(此外,这些细节可能会在未来版本的运行时中发生变化。)特别是,在x86/Debug情况下,我认为第一个Foo(0)实例被存储在寄存器或堆栈变量中,永远不会在方法的其余部分被覆盖;这个初始实例使整个链保持活动状态。在x86/Release和x64的情况下,我认为由于JIT优化,相同的寄存器或堆栈变量为每个实例重用,从而释放初始实例。