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 WeakReference
s? 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 String
s. 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
}