19

I'd like to verify that code setting up a WeakReference does not accidentally hold a strong reference to the referenced object. (Here's an example of how it is easy to accidentally do this.)

Does this look like the best way to check for inadvertent strong references?

TestObject testObj = new TestObject();
WeakReference wr = new WeakReference(testObj);

// Verify that the WeakReference actually points to the intended object instance.
Assert.Equals(wr.Target, testObject);

// Force disposal of testObj;
testObj = null;
GC.Collect();
// If no strong references are left to the wr.Target, wr.IsAlive will return false.
Assert.False(wr.IsAlive);
Community
  • 1
  • 1
Ben Gribaudo
  • 5,057
  • 1
  • 40
  • 75
  • You cannot expect GC.Collect(), to force the gc to collect garbage, it is just a suggestion, so it might not remove the object. [Automatic Memory Collection in .Net](http://msdn.microsoft.com/en-us/library/f144e03t.aspx) – Yet Another Geek May 06 '11 at 14:28
  • Would you mind elaborating on why GC.Collect() might not destroy an object that is eligible for collection? – Ben Gribaudo May 10 '11 at 19:08
  • Apparantly it forces in default mode. It is only when it is set in optimized mode it did not, I did not realize that. – Yet Another Geek May 11 '11 at 19:43
  • Hmm, it was only an suggestion in Java(the gc() method). Maybe I just thought it would do the same in C#. – Yet Another Geek May 11 '11 at 19:54
  • @BenGribaudo I have seen a strange behaviour that if instead of Assert.False(wr.IsAlive);I use if(wr.IsAlive) Console.WriteLine("Memory Leak"); I do see "Memory Leak" being printed and verified that testObj instance doesn't gets garbage collected in this case and hence the error. I even added GC.WaitForPendingFinalizers(); GC.Collect(); after the GC.Collect(); above. This works only when this check if inside the Debug.Assert or Assert. – Rajesh Nagpal Feb 19 '17 at 13:56

3 Answers3

13

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?

Palec
  • 12,743
  • 8
  • 69
  • 138
Ben Gribaudo
  • 5,057
  • 1
  • 40
  • 75
10

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!");
}
aaronburro
  • 504
  • 6
  • 15
7

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
}
n8wrl
  • 19,439
  • 4
  • 63
  • 103
  • The code doesn't *ensure* that the object is collected, you can't force the GC that way. You can still use `IsAlive` if you do a double check, i.e. also check for null after getting the target. This can be useful if you want the check to skip early, without having to do the casting if it's certain that there is nothing to cast. – Guffa May 06 '11 at 14:37
  • 4
    GC.Collect() preforms a blocking collection--the method only returns after the collection has completed, so the call to GC.WaitForFullGCComplete() should be unnecessary. GC.WaitForFullGCComplete() is intended for use in a different scenario. – Ben Gribaudo Jun 05 '11 at 00:17