没有为一次性类型对象调用终结器

时间:2017-02-19 16:13:17

标签: c# .net memory-leaks garbage-collection

重现步骤:

这是我的代码:

    using (TestClass test = new TestClass())
    {

    }

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

这就是我定义TestClass的方法:

public class TestClass : IDisposable
{
    public void Dispose()
    {
        Dispose(true);
        //GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            Console.WriteLine("Disposing TestClass.");
        }
    }

    ~TestClass()
    {
        Dispose(false);
        Console.WriteLine("TestClass Finalizer called");
    }
}

预期行为: 我确实看到“Disposing TestClass”在using语句之后按预期打印,但我也希望在运行GC命令后打印“TestClass Finalizer called”。我确保我跳过调用GC.SuppressFinalize(this);在Dispose方法中。看似已处置的变量即使在超出范围之后也无法最终确定。它们似乎在程序退出之前就已经完成了。

实际行为 我只看到在使用语句之后打印“Disposing TestClass”,只是在GC命令之后才看到“TestClass Finalizer被调用”。我只是在程序退出之前才看到它。

这不是内存泄漏吗?

如果我将它转换为非一次性类并更新如下代码,我确实看到在GC命令之后调用终结器。

TestClass test = new TestClass();
test = null;

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


public class TestClass
{
    ~TestClass()
    {   
        Console.WriteLine("TestClass Finalizer called");
    }
}

4 个答案:

答案 0 :(得分:4)

根据Microsoft对Object.Finalize Method ()

的描述
  

终结器执行的确切时间未定义。要确保为类的实例确定性地释放资源,请实现Close方法或提供IDisposable.Dispose实现。

将终结者视为第二道防线。如果程序未能拨打CloseDispose,则终结器将进行更改以更正遗漏。

这仍然可以确保文件在程序退出时关闭,例如,除非另一个终结器没有退出,因此阻止其他终结器或者如果灾难性异常残忍地终止该程序。

这不是内存泄漏。如果内存变得稀缺,垃圾收集器(GC)可以决定在程序退出之前很久就释放资源并调用终结器。

答案 1 :(得分:2)

调用Dispose方法不会影响对象的运行时。 GC仅收集不再引用的对象(例外情况适用)。在您的第一个示例中,test变量永远不会被垃圾收集(通过您的GC.Collect语句),因为它在包含方法内部而不是在使用块的范围内声明。

给出以下C#输入(A实施IDisposable):

using (var a = new A())
{

}

发出以下IL代码:

IL_0001: newobj       instance void Testit.A::.ctor()
IL_0006: stloc.0      // a
.try
{

  // [10 13 - 10 14]
  IL_0007: nop          

  // [12 13 - 12 14]
  IL_0008: nop          
  IL_0009: leave.s      IL_0016
} // end of .try
finally
{

  IL_000b: ldloc.0      // a
  IL_000c: brfalse.s    IL_0015
  IL_000e: ldloc.0      // a
  IL_000f: callvirt     instance void [mscorlib]System.IDisposable::Dispose()
  IL_0014: nop          
  IL_0015: endfinally   
} // end of finally

如果我们将它转​​换为C#,那将是〜结果:

A a;
try
{
}
catch
{
    if (a != null)
    {
        a.Dispose();
    }
}

变量a具有方法范围,因此仅在保留方法时才收集。这就是没有调用终结器的原因。

在第二个示例中,您故意将实例设置为null从范围中删除实例,因为没有活动指针可以收集实例。

答案 2 :(得分:0)

我想尽管你把测试变量设置为null仍然有一个对它的引用,它在调试模式下的某些.NET版本上发生。 试试这段代码

private void CreateAndRelease()
{
  new TestClass();
}

public void Main()
{
  CreateAndRelease();
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.ReadLine();
}

请注意,GC.WaitForPendingFinalizers被调用两次 - 首先GC.Collect不会最终确定你的对象,而只是将它放入终结队列,只有在下一次垃圾收集时才能完成它。

修改 一些害羞的人低估了我的答案,但我发布之前检查了它不像他们。 .NET 4.5.1调试模式

代码和输出1

Output:
Disposing TestClass.
Finished

public static void Main()
{
  using (TestClass test = new TestClass())
  {}

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

  Console.WriteLine("Finished");
  Console.ReadLine();
}

代码和输出2

Disposing TestClass.
TestClass Finalizer called
Finished

public static void Main()
{
  CreateAndRelease();

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

  Console.WriteLine("Finished");
  Console.ReadLine();
}

private static void CreateAndRelease()
{
  using (TestClass test = new TestClass())
  {}
}

所以我的答案绝对正确,并将有助于主题启动。

答案 3 :(得分:0)

以下是此行为的原因:

以下代码:

label1

获取编译为:

using (TestClass test = new TestClass())
{

}

出于与Strange behaviour while Unit testing for memory leak using WeakReference中所述相同的原因,当您在调试模式下运行时,"测试"变量是有效的,直到方法结束,因此它不是GC,并且终结器没有被调用。如果您在发布模式下运行相同的东西,它将按预期工作。

对于非一次性类型(在我的第二部分中),因为有一种方法可以指定这个" test"变量为null,因此GC即使在调试模式下使用也可以收集它,因此它可以按预期工作。