控制台应用程序和单元测试方法之间的不同垃圾收集行为

时间:2013-04-21 15:46:05

标签: c# .net garbage-collection

我偶然发现垃圾收集似乎在运行与单元测试相同的代码与在控制台应用程序的Main方法中编写的相同代码之间表现不同。我想知道这种差异背后的原因。

在这种情况下,我和同事对于在垃圾收集上注册事件处理程序的效果存在分歧。我认为演示比仅仅向他发送highly rated SO answer的链接更好。因此,我写了一个简单的演示作为单元测试。

我的单元测试显示事情正常,因为我说他们应该。但是,我的同事写了一个控制台应用程序,显示事情正在发挥作用,这意味着GC没有按照我在Main方法中对本地对象的预期发生。通过将我的测试中的代码移动到控制台应用程序项目的Main方法中,我能够重现他所看到的行为。

我想知道的是,当在控制台应用程序的Main方法中运行时,GC似乎没有按预期收集对象。通过提取方法以便在不同方法中调用GC.Collect和超出范围的对象,恢复了预期的行为。

这些是我用来定义测试的对象。只有一个带有事件的对象和一个为事件处理程序提供合适方法的对象。两者都有终结器设置一个全局变量,以便您可以判断它们何时被收集。

private static string Log;
public const string EventedObjectDisposed = "EventedObject disposed";
public const string HandlingObjectDisposed = "HandlingObject disposed";

private class EventedObject
{
    public event Action DoIt;

    ~EventedObject()
    {
        Log = EventedObjectDisposed;
    }

    protected virtual void OnDoIt()
    {
        Action handler = DoIt;
        if (handler != null) handler();
    }
}

private class HandlingObject
{

    ~HandlingObject()
    {
        Log = HandlingObjectDisposed;
    }

    public void Yeah()
    {
    }
}

这是我的测试(NUnit),它通过:

[Test]
public void TestReference()
{
    {
        HandlingObject subscriber = new HandlingObject();

        {
            {
                EventedObject publisher = new EventedObject();
                publisher.DoIt += subscriber.Yeah;
            }

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();
            Thread.MemoryBarrier();

            Assert.That(Log, Is.EqualTo(EventedObjectDisposed));
        }

        //Assertion needed for foo reference, else optimization causes it to already be collected.
        Assert.IsNotNull(subscriber);
    }

    GC.Collect(GC.MaxGeneration);
    GC.WaitForPendingFinalizers();
    Thread.MemoryBarrier();

    Assert.That(Log, Is.EqualTo(HandlingObjectDisposed));
}

我将上面的主体粘贴到新控制台应用程序的Main方法中,并将Assert调用转换为Trace.Assert调用。两个相等的断言失败然后失败。如果需要,生成的Main方法代码为here

我确实认识到,当GC发生时应该被视为非确定性的,并且通常应用程序不应该关注它何时发生。 在所有情况下,代码都是在发布模式下编译的,目标是.NET 4.5。

编辑:我尝试过的其他事情

  • 由于NUnit支持,因此制作测试方法static;测试仍然有效。
  • 我还尝试将整个Main方法解压缩到程序中的实例方法并调用它。两个断言仍然失败。
  • Main[STAThread][MTAThread]归因于this有所不同。两个断言仍然失败。
  • 根据@ Moo-Juice的建议:
    • 我将NUnit引用到Console应用程序,以便我可以使用NUnit断言,但它们失败了。
    • 我尝试了对测试,测试类,Main方法和包含Main方法静态的类的可见性的各种更改。没有变化。
    • 我尝试将Test类设为static,并将包含Main方法的类设为static。没有变化。

2 个答案:

答案 0 :(得分:6)

如果将以下代码提取到单独的方法,则测试更有可能按预期运行。编辑:请注意,即使您将代码提取到单独的方法,C#语言规范的措辞也不需要通过此测试。

        {
            EventedObject publisher = new EventedObject();
            publisher.DoIt += subscriber.Yeah;
        }

规范允许但不要求publisher在此块结束时立即有资格获得GC,因此您不应该以这样的方式编写代码可以在这里收集。

来自ECMA-334的

编辑(C#语言规范)§10.9自动内存管理(强调我的)

  

如果任何可能的继续执行都无法访问对象的任何部分,除了运行终结器之外,该对象被认为不再使用,并且它有资格进行最终化。 [注意:实现可能会选择分析代码以确定将来可以使用对对象的哪些引用。例如,如果作用域中的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则实现可能(但不要求将对象视为不再使用。结束说明]

答案 1 :(得分:1)

问题不在于它是一个控制台应用程序 - 问题是您可能通过Visual Studio运行它 - 连接了调试器!和/或您正在将控制台应用程序编译为Debug构建。

确保您正在编译发布版本。然后转到Debug -> Start Without Debugging,或按Ctrl + F5,或从命令行运行控制台应用程序。垃圾收集器现在应该按预期运行。

这也是Eric Lippert提醒您不要在C# Performance Benchmark Mistakes, Part One的调试器中运行任何性能基准的原因。

  

jit编译器知道附加了一个调试器,它故意对它生成的代码进行去优化,以便于调试。垃圾收集器知道附加了调试器;它与jit编译器一起使用,以确保内存不那么积极地清理,这可能会在某些情况下极大地影响性能。

Eric系列文章中的很多提醒都适用于您的场景。如果您有兴趣阅读更多内容,请参阅以下twothreefour部分的链接。