【问题标题】:Garbage collection of inaccessible objects in loop循环中不可访问对象的垃圾收集
【发布时间】:2014-06-11 02:41:57
【问题描述】:

如果我有这样的循环:

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() 时立即下降。

【问题讨论】:

  • 什么是“父对象”?
  • “在循环退出之前,垃圾收集器不会清理父对象”:您能在问题中描述您是如何进行这些观察的吗?
  • @quantdev 我编辑了这个问题。
  • 我知道这不是你的真实代码,但为什么不直接使用foo = new Foo() 而不是分配给foo.Foo?顺便说一句,Public Foo Foo; 无法编译。
  • 您在任务管理器中查看的列的名称是什么?

标签: c# .net garbage-collection


【解决方案1】:

这是一个稍作修改的程序,可以更清楚地演示该行为:

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 上,当我在调试模式下构建时 AND 以任何 CPU 或 x86 为目标,我会重现您所看到的行为:直到打印“Exiting”之后,实例才会最终确定.但是当我在发布模式目标 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 优化,same 寄存器或堆栈变量被每个实例重用,从而释放初始实例。

【讨论】:

  • 有关如何/为什么调试模式下的行为不同的更多信息?
  • @user2864740 编译器/JIT 生成额外的临时文件以帮助调试。当您在“监视”窗口中查看本地人时,您不希望他们被收集。
  • True .. 但在这种情况下,似乎需要存储一个集合(不仅仅是一两个额外的临时/对象)以保持对象的强可达性:|
  • 由于实例是链接的,只需要存储一个对Foo(0)的引用。但是,我不知道它是如何或为什么这样做的。将循环更改为简单的foo = new Foo(value); 会立即收集所有实例,即使在调试模式下也是如此。
  • 我还注意到,在附加调试器的情况下启动时,内存在 Release 模式下会继续增长,因此肯定涉及 JIT。
猜你喜欢
  • 2013-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-18
  • 2010-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多