Testing/Verifying a WeakReference

Unit tests involving WeakReference objects are trickier than you might expect. As you and others have noted, GC.Collect() can presumably "force" a garbage collection, but that still depends upon your object having no references to it.

Unfortunately, how you build your code can change whether objects still have references to them. More specifically, whether you are building in Debug or Release mode can and will change when objects are still rooted (more accurately, in depends on whether you have optimizations turned on; Debug defaults to having them off, while Release defaults to having them on). Debug mode turns off a lot of optimizations, and it even has a tendency to root objects that were created/declared in the method that is currently being executed. So, your unit tests may fail in Debug builds, but succeed in Release builds.

In your example, even though you set testObj to NULL, the compiler is trying to be helpful in a Debug build by keeping its previous value rooted. That means that no matter how many times you call GC.Collect(), wr.IsAlive will always return TRUE.

So, how the heck can you test WeakReferences? Simple: create them AND the objects they are based off of in another method. As long as that method doesn't get in-lined, and for the most part, it won't, the compiler won't root the object you care about, and you can have your tests pass in both Debug and Release builds.

The function below gives you a hint as to how to do this:

public static Tuple<WeakReference, ManualResetEvent, int> GetKillableWr(Func<object> func, bool useGetHashCode = false)
{
    var foo = func();
    var result = new Tuple<WeakReference, ManualResetEvent, int>(new WeakReference(foo), new ManualResetEvent(false), useGetHashCode ? (foo?.GetHashCode() ?? 0) : RuntimeHelpers.GetHashCode(foo));

    Task.Factory.StartNew(() =>
    {
        result.Item2.WaitOne();
        GC.KeepAlive(foo);  // need this here to make sure it doesn't get GC-ed ahead of time
        foo = null;
    });

    return result;
}

Using this, as long as you create your object inside the func parameter, you can create a WeakReference to an object of your choosing that won't be rooted after you signal the returned ManualResetEvent and call GC.Collect(). As others have noted, it can be helpful to call the below code to ensure cleanup happens as you need it...

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

EDIT:

There are some other "gotcha's" to worry about. A common one involves Strings. String literals and constants are always rooted, because they are compiled as a reference into your DLL/EXE. So, something like new WeakReference("foo") will always show as being alive, because "foo" has been stored into your DLL and a reference to that stored literal is provided in the compiled code. An easy way around this is to use new StringBuilder("<your string here>").ToString() instead of the string literal.

EDIT AGAIN:

Another "gotcha" is that in Release builds, optimizations cause the GC to be more aggressive, which, unlike the above scenarios, might cause objects to go out of scope sooner than you expect. In the code below, wr.IsAlive can sometimes return FALSE, because the GC has detected that myObject will not be used by anything else in the method, so it made it eligible for garbage collection. The way around this is to put GC.KeepAlive(myObject) at the end of your method. That will keep myObject rooted until at least that line is executed.

public static void SomeTest()
{
    var myObject = new object();
    var wr = new WeakReference(myObject);
    GC.Collect();
    Assert.True(wr.IsAlive, "This could fail in Release Mode!");
}

I got in touch with Microsoft about this and learned/confirmed that:

  • GC.Collect() forces a blocking garbage collection.
  • When GC.Collect() runs, it won't mysteriously skip over collection-eligible objects. Predictable rules are followed for determining which objects to collect. As long as you operate with an understanding of those rules (i.e. how finalizable objects are handled), you can force a particular object to be destroyed though the memory used by the destroyed object may or may not be freed.

More information on my blog: Can .Net garbage collection be forced?


I did this just yesterday. Here's what I had to add to ensure the collection happened prior to your last assert:

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.WaitForFullGCComplete();
        GC.Collect();

If after this .IsAlive is still true, it's likely there is still a strong reference somewhere.

Incidentally - Be sure to NOT check .IsAlive when you access your WeakReference target. To avoid a race condition between you checking .IsAlive and .Target, do this:

var r = weakRef.Target AS Something;
if (r != null)
{
    ... do your thing
}