Testing GC Eligibility of Objects in C#

This posts talks about how to determine whether an object in C# is eligible for garbage collection or not (i.e. determine its GC eligibility).

Two garbage cans in front of rock formations in a desert landscape
Garbage Collection in the Monument Valley Navajo Tribal Park

Introduction

Garbage Collection (GC) is very handy when developing applications as it takes the burden of manual memory management off from the programmer. It is still possible, however, to create memory leaks. For example, statically referencing an object prevents the Garbage Collector from collecting it. Thus when using Test Driven Development (TDD), the need to verify GC Eligibility in a unit test arises. The GCWatch class introduced in this posting provides this ability.

Usage

For this example, we are verifying some basic concepts of garbage collection using three different tests which illustrate the usage of the GCWatch class. The tests are based on the NUnit testing framework.

  • Holding an accessible reference to an object should prevent garbage collection:
    [Test]
    public void Usable_Reference_Keeps_Object_Alive()
    {
        var obj = new Object();
        var gcTester = new GCWatch(obj);
    
        Assert.IsFalse(gcTester.IsEligibleForGC());
    
        GC.KeepAlive(obj);
    }

    Note the call to GC.KeepAlive(obj); which ensures that the reference is actually used (see below for a more detailed explanation).

  • An unreferenced object is eligible for garbage collection
    [Test]
    public void No_Reference_Allows_GC()
    {
        var obj = new Object();
        var gcTester = new GCWatch(obj);
    
        obj = null;
    
        Assert.IsTrue(gcTester.IsEligibleForGC());
    }
  • Objects holding references to each other are still eligible for garbage collection if they are not referenced from the outside:

    [Test]
    public void GC_Collects_Graph()
    {
        var foo1 = new Foo();
        var foo2 = new Foo();
    
        var gcTester1 = new GCWatch(foo1);
        var gcTester2 = new GCWatch(foo2);
    
        foo1.objects.Add(foo2);
        foo2.objects.Add(foo1);
    
        foo1 = null;
        Assert.IsFalse(gcTester1.IsEligibleForGC());
        Assert.IsFalse(gcTester2.IsEligibleForGC());
        GC.KeepAlive(foo2);
    
        foo2 = null;
        Assert.IsTrue(gcTester1.IsEligibleForGC());
        Assert.IsTrue(gcTester2.IsEligibleForGC());
    }

What about unused references?

The first test includes a call to GC.KeepAlive(obj); which I described is necessary to have a “usable” reference. What happens when removing this call? Is the object eligible for garbage collection at the point of the assert or not?

var obj = new Object();
var gcTester = new GCWatch(obj);

Assert.Is???(gcTester.IsEligibleForGC()); //IsTrue or IsFalse?

The correct answer is that it depends on the project configuration whether the object will be eligible for garbage collection at the end of the method. As discussed in When do I need to use GC.KeepAlive? (which also describes the purpose of GC.KeepAlive – in short, it’s a way of referencing or “using” a variable making sure that the optimizer won’t optimize the usage away), the garbage collector might decide to collect objects as soon as they are not usable by any executing code anymore. This can very well happen in situations where it would be valid to access a reference (at compile time), but no such code has been written.

However, when compiling and executing code in Debug-mode, the compiler prevents this from happening to ease debugging. As a result, the correct implementation of our test method includes a preprocessor directive:

[Test]
public void Reference_Keeps_Object_Alive_Or_Not()
{
    var obj = new Object();
    var gcTester = new GCWatch(obj);

#if DEBUG
    Assert.IsFalse(gcTester.IsEligibleForGC());
#else
    Assert.IsTrue(gcTester.IsEligibleForGC());
#endif
}

Implementation

The implementation of the GCWatch class is quite simple. During construction, a WeakReference to the object is crated (which allows access to an object while not preventing garbage collection):

public GCWatch(object value)
{
    this._reference = new WeakReference(value);
}

When determining the eligibility for garbage collection, a full garbage collection is performed (by calling GC.Collect) which forces all objects eligible for garbage collection to be collected. Afterwards, we simply test whether the weak reference is still alive (i.e. the object has not been collected and thus had not been eligible for garbage collection).

public bool IsEligibleForGC()
{
    if (!this._reference.IsAlive)
        return true;

    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
    GC.WaitForPendingFinalizers();
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

    return !this._reference.IsAlive;
}

Furthermore, note that the call to GC.WaitForPendingFinalizers(); followed by a second, full garbage collection is necessary to fully support graphs containing objects providing a finalizer method as they are first placed in the so-called FReachable queue. This is best illustrated in another test case:

[Test]
public void Two_Collections_Are_Required()
{
    var foo = new Foo();
    var bar = new Bar();
    bar.objects.Add(foo);

    var weakReference = new WeakReference(foo, true);
    foo = null;
    bar = null;

    Assert.IsTrue(weakReference.IsAlive);

    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

    //At this point, bar has been collected, but placed into the FReachable queue. Thus, foo is still accessible (from the finalizer of foo)
    //Note: Might also be false (when additional, non-forced GC has occured - very unlikely, however)
    Assert.IsTrue(weakReference.IsAlive);

    GC.WaitForPendingFinalizers();
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

    //Now that bar has been completely collected, foo could also be collected
    Assert.IsFalse(weakReference.IsAlive);
}

Code

You can find the code on GitHub.