数组中的对象不会被垃圾收集

时间:2011-11-11 08:36:05

标签: c# .net garbage-collection

我正在测试一个使用弱引用的类,以确保对象能够被垃圾收集,并且我在List<>中找到了对象。即使列表不再被引用,也从未收集过。简单数组也是如此。以下代码段显示了一个失败的简单测试。

class TestDestructor
{
    public static bool DestructorCalled;

    ~TestDestructor()
    {
        DestructorCalled = true;
    }
}

[Test]
public void TestGarbageCollection()
{
    TestDestructor testDestructor = new TestDestructor();

    var array = new object[] { testDestructor };
    array = null;

    testDestructor = null;

    GC.Collect();
    GC.WaitForPendingFinalizers();

    Assert.IsTrue(TestDestructor.DestructorCalled);
}

退出阵列的初始化会导致测试通过。

为什么数组中的对象没有被垃圾收集?

5 个答案:

答案 0 :(得分:4)

编辑:好的,我在这方面取得了一些进展。有三个二进制开关可以参与(至少):

  • 代码是否优化;即命令行上的/o+/o-标志。这似乎没什么区别。
  • 代码是否在 调试器中运行。这似乎没什么区别。
  • 生成的调试信息级别,即/debug+/debug-/debug:full/debug:pdbonly命令行标志。只有/debug+/debug:full会导致其失败。

此外:

  • 如果您将Main代码与TestDestructor代码分开,则可以告诉它是Main代码的编译模式,这会产生差异
  • 据我所知,为/debug:pdbonly生成的IL与方法本身中的/debug:full 相同,因此它可能是一个明显的问题......
编辑:好的,现在真的很奇怪。如果我拆解“破损”版本然后重新组装它,它可以工作:

ildasm /out:broken.il Program.exe
ilasm broken.il

ilasm有三种不同的调试设置:/DEBUG/DEBUG=OPT/DEBUG=IMPL。使用前两个中的任何一个,它都失败了 - 使用最后一个,它可以工作。最后一个被描述为启用JIT优化,所以大概这就是在这里有所作为......虽然在我看来它 仍然能够以任何方式收集对象。


这可能是由DestructorCalled方面的内存模型引起的。它不是易失性的,因此无法保证终结器线程的写入被测试线程“看到”。

在这种情况下,终结者肯定会 。在使变量变为volatile之后,这个独立的等效示例(对我来说简单易用)肯定会为我打印True。当然,这不是证明:没有volatile代码不能保证失败;它不能保证工作。 将其作为易变变量后,你能否让你的测试失败?

using System;

class TestDestructor
{
    public static volatile bool DestructorCalled;

    ~TestDestructor()
    {
        DestructorCalled = true;
    }
}

class Test
{
    static void Main()
    {
        TestDestructor testDestructor = new TestDestructor();

        var array = new object[] { testDestructor };
        array = null;

        testDestructor = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(TestDestructor.DestructorCalled);
    }
}
编辑:我刚刚看到这个在使用Visual Studio构建时失败,但从命令行开始就没问题。现在看看IL ......

答案 1 :(得分:2)

另一个编辑:如果数组是在Main() - Method-Scope中定义的,则结果将始终为false,但如果在Class-Test-Scope中定义,则结果为true。也许这不是一件坏事。

class TestDestructor
{
    public TestDestructor()
    {
        testList = new List<string>();
    }

    public static volatile bool DestructorCalled;

    ~TestDestructor()
    {
        DestructorCalled = true;
    }

    public string xy = "test";

    public List<string> testList;

}

class Test
{
    private static object[] myArray;

    static void Main()
    {
        NewMethod();            
        myArray = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(TestDestructor.DestructorCalled);
        Console.In.ReadToEnd();
    }

    private static void NewMethod()
    {
        TestDestructor testDestructor = new TestDestructor() { xy = "foo" };
        testDestructor.testList.Add("bar");
        myArray = new object[] { testDestructor };
        Console.WriteLine(myArray.Length);
    }
}

答案 2 :(得分:1)

正如Ani在评论中指出的那样,整个数组在发布模式下进行了优化,因此我们应该将代码更改为:

class TestDestructor
{
    public static bool DestructorCalled;
    ~TestDestructor()
    {
        DestructorCalled = true;
    }
}

class Test
{
    static void Main()
    {
        TestDestructor testDestructor = new TestDestructor();

        var array = new object[] { testDestructor };
        Console.WriteLine(array[0].ToString());
        array = null;

        testDestructor = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(TestDestructor.DestructorCalled);
    }
} 

对我来说它有效(没有不稳定)并且始终打印为True。任何人都可以确认在释放模式下没有调用终结器,因为否则我们可以假设它与调试模式有关。

答案 3 :(得分:0)

如果我没弄错的话,那是因为对象基本上是复制的,并且在加载到数组中时与其初始创建分开。然后,当您销毁数组和原始对象时,复制到数组的对象仍然存在。

垃圾收集应该最终完成它的工作,但我得到你试图强制它清除资源。我要尝试的是在销毁之前首先清除数组(删除对象),然后查看是否删除了所有内容。

答案 4 :(得分:0)

这就是documentation所说的:

  

实现Finalize方法或析构函数会对性能产生负面影响,您应该避免不必要地使用它们。使用Finalize方法回收对象使用的内存至少需要两个垃圾回收。 [...]未来的垃圾收集将确定最终的对象是真正的垃圾,因为标记为准备完成的对象列表中的条目不再指向它们。在未来的垃圾收集中,对象的内存实际上是回收的。

尝试使用处理机制而不是最终确定将会发生什么