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 theMain
method static. No change. - I tried making the Test class static and the class containing the
Main
method static. No change.