7

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.
Community
  • 1
  • 1
vossad01
  • 11,552
  • 8
  • 56
  • 109
  • What is the result of when you use `GC.Collect()` with its default parameters and do not specify a generation? – Moo-Juice Apr 21 '13 at 15:57
  • @Moo-Juice Test still passes, Console Application asserts still fail, extracting methods from Console Application still causes asserts to succeed. – vossad01 Apr 21 '13 at 16:04
  • I wonder if the difference is due to the fact that `Main` is within a static class... I am not sure, but other than that and the behaviour of `Trace` vs `Assert` I am out of ideas. Have you tried running the test itself from within a static context? – Moo-Juice Apr 21 '13 at 16:21
  • @Moo-Juice edited question to list other things I tried as well as results from trying new things based on your comment. Short answer is no change in results. – vossad01 Apr 21 '13 at 16:54
  • 1
    Did you compile your console app as Debug or Release build? I know that when a debugger is attached the lifetime of variables is extended until the method is left. There could be something similar going on when the hosting executable was built as debug or release binary. – Alois Kraus Apr 21 '13 at 20:50
  • 1
    This question is incoherent. You say that you know that the GC is nondeterministic and that its choices are implementation-defined, and then you express that you have an *expectation of certain behaviour*, and are surprised when the GC behaves differently in different scenarios. These beliefs are contradictory; what are you really asking in this question? – Eric Lippert May 14 '14 at 16:03
  • @EricLippert I know automatic GC is non-deterministic. Where I understand my having gone wrong was in believing I could have expectations of certain behavior when using methods from `GC`. Such as expecting `GC.Collect` would collect everything eligible according to the C# spec. It does look like I should see if I can find the solution again to test the debugger aspect as I no longer recall how I ran it. – vossad01 May 14 '14 at 19:54
  • @vossad01: Well then your error is assuming that the C# specification says anything whatsoever on the subject about what the garbage collector **can be expected to do**. The C# specification by contrast actually says that the garbage collector has **arbitrarily broad lattitude to collect objects that are referenced by variables either earlier or later than when control passes out of the scope of a variable ending its lifetime**. – Eric Lippert May 14 '14 at 20:44
  • Specifically I draw your attention to `The garbage collector is allowed wide latitude in deciding when to collect objects and run destructors.` and `the garbage collector may choose to analyze code to determine which references to an object may be used in the future` and `the garbage collector may implement a wide range of memory management policies` and `C# does not require that destructors be run or that objects be collected as soon as they are eligible` ... – Eric Lippert May 14 '14 at 20:48
  • 1
    ... and `Since the garbage collector is allowed wide latitude in deciding when to collect objects and run destructors, a conforming implementation may produce output that differs from that shown` and so on. The specification goes out of its way to say **multiple times** that you cannot rely on the garbage collector having any particular behaviour under any particular circumstances. – Eric Lippert May 14 '14 at 20:49

2 Answers2

6

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]

Sam Harwell
  • 97,721
  • 20
  • 209
  • 280
  • Just to make sure I understand, the spec does _require_ `publisher` be available for GC if that code bit of is in a separate method, but not if it is only within a code block surrounded by `{` `}`? – vossad01 Apr 21 '13 at 20:56
  • @vossad01 Yes, you are correct. However, it does not make any requirements regarding when the finalizer is run, even if `GC.Collect` is called. Therefore, the test could fail even if the code was relocated and it would not indicate a bug in the C# compiler or .NET implementation. – Sam Harwell Apr 21 '13 at 21:00
  • Does not `GC.WaitForPendingFinalizers` ensure that the finalizer is run because `GC.Collect` does not? – vossad01 Apr 21 '13 at 21:35
  • I was accepting of nested blocks having different requirements than local method calls until I started diffing into into that specification after your edit. If it is not _required_ after a block I do not see why it would be _required_ after a method call. According to 10.7 and 15.2, the scope of a local variable is the block in which it is defined. The above is a block just as a method-body contains a block (17.5), so `publisher`'s scope is bound to it and thus can be considered "no longer in use" once that block is left as it is then out of scope. Am I missing something? – vossad01 Apr 21 '13 at 21:38
  • 1
    @vossad01 Think of how the behavior would be if `GC.Collect` simply returned without doing anything. That would be perfectly valid implementation of the method if your program is written in C#. And that is why even if you get your unit test to pass under whatever test configuration you are using, it will still be flawed. – Sam Harwell Apr 21 '13 at 22:38
  • @vossad01 You're missing one important point - code blocks are only there in C#, the resulting IL code doesn't have them. They have no relation to what the JIT (or the GC) does in runtime. In reality, in release mode, local variables can be released as soon as they're not needed. On the other hand, in debug mode, and especially in debugger, they will be kept as long as possible to enable reasonable debugging. Try running the console application in release mode and outside of the debugger. Oh, and call `GC.Collect` again after the `WaitForPendingFinalizers` call. – Luaan May 13 '14 at 08:20
1

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.

dcastro
  • 66,540
  • 21
  • 145
  • 155