Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different Garbage Collection behavior between Console Application and Unit Test Method

I have stumbled across a situation where garbage collection seems to be behaving differently between the same code running written as a Unit Test vs written in the Main method of a Console Application. I am wondering the reason behind this difference.

In this situation, a co-worker and I were in disagreement over the effects of registering an event handler on garbage collection. I thought that a demonstration would be better accepted than simply sending him a link to a highly rated SO answer. As such I wrote a simple demonstration as a unit test.

My unit test showed things worked as I said they should. However, my coworker wrote a console application that showed things working his way, which meant that GC was not occurring as I expected on the local objects in the Main method. I was able to reproduce the behavior he saw simply by moving the code from my test into the Main method of a Console Application project.

What I would like to know is why GC does not seem to collecting objects as expected when running in the Main method of a Console Application. By extracting methods so that the call to GC.Collect and the object going out of scope occurred in different methods, the expected behavior was restored.

These are the objects I used to define my test. There is simply an object with an event and an object providing a suitable method for an event handler. Both have finalizers setting a global variable so that you can tell when they have been collected.

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()
    {
    }
}

This is my test (NUnit), which passes:

[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));
}

I pasted the body above in to the Main method of a new console application, and converted the Assert calls to Trace.Assert invocations. Both equality asserts fail then fail. Code of resulting Main method is here if you want it.

I do recognize that when GC occurs should be treated as non-deterministic and that generally an application should not be concerning itself with when exactly it occurs. In all cases the code was compiled in Release mode and targeting .NET 4.5.

Edit: Other things I tried

  • Making the test method static since NUnit supports that; test still worked.
  • I also tried extracting the whole Main method into an instance method on program and calling that. Both asserts still failed.
  • Attributing Main with [STAThread] or [MTAThread] in case this made a difference. Both asserts still failed.
  • Based on @Moo-Juice's suggestions:
    • I referenced NUnit to the Console app so that I could use the NUnit asserts, they failed.
    • I tried various changes to visibility to the both the test, test's class, Main method, and the class containing the Main method static. No change.
    • I tried making the Test class static and the class containing the Main method static. No change.
like image 203
vossad01 Avatar asked Apr 21 '13 15:04

vossad01


2 Answers

If the following code was extracted to a separate method, the test would be more likely to behave as you expected. Edit: Note that the wording of the C# language specification does not require this test to pass, even if you extract the code to a separate method.

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

The specification allows but does not require that publisher be eligible for GC immediately at the end of this block, so you should not write code in such a way that you are assuming it can be collected here.

Edit: from ECMA-334 (C# language specification) §10.9 Automatic memory management (emphasis mine)

If no part of the object can be accessed by any possible continuation of execution, other than the running of finalizers, the object is considered no longer in use and it becomes eligible for finalization. [Note: Implementations might choose to analyze code to determine which references to an object can be used in the future. For instance, if a local variable that is in scope is the only existing reference to an object, but that local variable is never referred to in any possible continuation of execution from the current execution point in the procedure, an implementation might (but is not required to) treat the object as no longer in use. end note]

like image 118
Sam Harwell Avatar answered Oct 08 '22 13:10

Sam Harwell


The problem isn't that it's a console application - the problem is that you're likely running it through Visual Studio - with a debugger attached! And/or you're compiling the console app as a Debug build.

Make sure you're compiling a Release build. Then go to Debug -> Start Without Debugging, or press Ctrl+F5, or run your console application from a command line. The Garbage Collector should now behave as expected.

This is also why Eric Lippert reminds you not to run any perf benchmarks in a debugger in C# Performance Benchmark Mistakes, Part One.

The jit compiler knows that a debugger is attached, and it deliberately de-optimizes the code it generates to make it easier to debug. The garbage collector knows that a debugger is attached; it works with the jit compiler to ensure that memory is cleaned up less aggressively, which can greatly affect performance in some scenarios.

Lots of the reminders in Eric's series of articles apply to your scenario. If you're interested in reading more, here are the links for parts two, three and four.

like image 34
dcastro Avatar answered Oct 08 '22 12:10

dcastro