15

I have a test that I expected to pass but the behavior of the Garbage Collector is not as I presumed:

[Test]
public void WeakReferenceTest2()
{
    var obj = new object();
    var wRef = new WeakReference(obj);

    wRef.IsAlive.Should().BeTrue(); //passes

    GC.Collect();

    wRef.IsAlive.Should().BeTrue(); //passes

    obj = null;

    GC.Collect();

    wRef.IsAlive.Should().BeFalse(); //fails
}

In this example the obj object should be GC'd and therefore I would expect the WeakReference.IsAlive property to return false.

It seems that because the obj variable was declared in the same scope as the GC.Collect it is not being collected. If I move the obj declaration and initialization outside of the method the test passes.

Does anyone have any technical reference documentation or explanation for this behavior?

Servy
  • 202,030
  • 26
  • 332
  • 449
TechnoTone
  • 181
  • 2
  • 6
  • 1
    Have you checked what the IL code looks like? Also, does it behave the same way for release and debug builds? – Matthew Watson Mar 04 '13 at 16:15
  • 3
    My initial guess is that compiler/runtime/processor optimizations are biting you. They realize you're never reading `obj` so it's allowed to re-order the operations among the other method calls. Try adding something like `Console.WriteLine(obj == null)` just to prevent the compiler from doing that. – Servy Mar 04 '13 at 16:15
  • 1
    This sample works fine on my machine. I'm using `Console.WriteLine` to log the `IsAlive` parameter though instead of `Should()` – JaredPar Mar 04 '13 at 16:15
  • 4
    Worth noting that it is not guaranteed that no strong reference to the object exists at that point. You should not be writing code that assumes the garbage collector will kill an object at any given time. – Jonathan Grynspan Mar 04 '13 at 16:20
  • 1
    [From MSDN](http://msdn.microsoft.com/en-us/library/xe0c2357.aspx) (emphasis mine): "Use this method to **try** to reclaim all memory that is inaccessible." – user7116 Mar 04 '13 at 16:21
  • Please add exact version of .Net you are using. Also try to see if code posted by Matthew Watson works differently for you. – Alexei Levenkov Mar 04 '13 at 16:28
  • When I executed your code on my machine, the results are as expected i.e. True, True, False.?? – Azhar Khorasany Mar 04 '13 at 16:40

6 Answers6

15

Hit the same issue as you - my test was passing everywhere, except for under NCrunch (could be any other instrumentation in your case). Hm. Debugging with SOS revealed additional roots held on a call stack of a test method. My guess is that they were a result of code instrumentation that disabled any compiler optimizations, including those that correctly compute object reachability.

The cure here is quite simple - don't ever hold strong references from a method that does GC and tests for aliveness. This can be easily achieved with a trivial helper method. The change below made your test case pass with NCrunch, where it was originally failing.

[TestMethod]
public void WeakReferenceTest2()
{
    var wRef2 = CallInItsOwnScope(() =>
    {
        var obj = new object();
        var wRef = new WeakReference(obj);

        wRef.IsAlive.Should().BeTrue(); //passes

        GC.Collect();

        wRef.IsAlive.Should().BeTrue(); //passes
        return wRef;
    });

    GC.Collect();

    wRef2.IsAlive.Should().BeFalse(); //used to fail, now passes
}

private T CallInItsOwnScope<T>(Func<T> getter)
{
    return getter();
}
glebkhol
  • 166
  • 1
  • 3
  • Thanks for this! A very elegant solution. I was using NCrunch too. – TechnoTone Jun 02 '16 at 21:22
  • 1
    This solution worked for me though I simplified it a bit. I put the "CallInItsOwnScope" operations in a separate function rather than in a lambda and this allowed the forced GC to perform as expected. – Kent Aug 11 '16 at 02:43
  • I encountered this issue (using MSTest to drive) - but only when the test was running in 64bit. Under 32Bit, the GC cleaned the objects despite being in the same scope. Can't explain why it would be different. – Rags Dec 11 '19 at 15:25
  • Same issue encountered testing with Xunit and running within JetBrains Rider. Lambda solution (and separate function solution suggested by Kent) works nicely. Thanks! – Alexis May 21 '20 at 15:15
10

There are a few potential issues I can see:

  • I am unaware of anything in the C# specification which requires that the lifetimes of local variables be limited. In a non-debug build, I think the compiler would be free to omit the last assignment to obj (setting it to null) since no code path would cause the value of obj will never be used after it, but I would expect that in a non-debug build the metadata would indicate that the variable is never used after the creation of the weak reference. In a debug build, the variable should exist throughout the function scope, but the obj = null; statement should actually clear it. Nonetheless, I'm not certain that the C# spec promises that the compiler won't omit the last statement and yet still keep the variable around.

  • If you are using a concurrent garbage collector, it would may be that GC.Collect() triggers the immediate start of a collection, but that the collection wouldn't actually be completed before GC.Collect() returns. In this scenario, it may not be necessary to wait for all finalizers to run, and thus GC.WaitForPendingFinalizers() may be overkill, but it would probably solve the problem.

  • When using the standard garbage collector, I would not expect the existence of a weak reference to an object to prolong the existence of the object in the way that a finalizer would, but when using a concurrent garbage collector, it's possible that abandoned objects to which a weak reference exists get moved to a queue of objects with weak references that need to be cleaned up, and that the processing of such cleanup happens on a separate thread that runs concurrently with everything else. In such case, a call to GC.WaitForPendingFinalizers() would be necessary to achieve the desired behavior.

Note that one should generally not expect that weak references will be invalidated with any particular degree of timeliness, nor should one expect that fetching Target after IsAlive reports true will yield a non-null reference. One should use IsAlive only in cases where one wouldn't care about the target if it's still alive, but would be interested in knowing that the reference has died. For example, if one has a collection of WeakReference objects, one may wish to periodically iterate through the list and remove WeakReference objects whose target has died. One should be prepared for the possibility that WeakReferences might remain in the collection longer than would be ideally necessary; the only consequence if they do so should be a slight waste of memory and CPU time.

supercat
  • 77,689
  • 9
  • 166
  • 211
4

As far as I know, calling Collect does not guarantee that all resources are released. You are merely making a suggestion to the garbage collector.

You could try to force it to block until all objects are released by doing this:

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

I expect that this might not work absolutely 100% of the time. In general, I would avoid writing any code that depends on observing the garbage collector, it is not really designed to be used in this way.

Alex Peck
  • 4,603
  • 1
  • 33
  • 37
3

This answer is not related to unit tests, but it might be helpful to somebody who's testing out weak references and wondering why they don't work as expected.

The issue is basically the JIT keeping the variables alive. This can be avoided by instantiating the WeakReference and the target object in a non-inlined method:

private static MyClass _myObject = new MyClass();

static void Main(string[] args)
{
    WeakReference<object> wr = CreateWeakReference();

    _myObject = null;

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

    wr.TryGetTarget(out object targetObject);

    Console.WriteLine(targetObject == null ? "NULL" : "It's alive!");
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference<object> CreateWeakReference()
{
    _myObject = new MyClass();
    return new WeakReference<object>(_myObject);
}

public class MyClass
{
}                                                            

Commenting out _myObject = null; will prevent garbage collection of that object.

Shahin Dohan
  • 6,149
  • 3
  • 41
  • 58
2

Could it be that the .Should() extension method is somehow hanging on to a reference? Or perhaps some other aspect of the test framework is causing this issue.

(I'm posting this as an answer otherwise I can't easily post the code!)

I have tried the following code, and it works as expected (Visual Studio 2012, .Net 4 build, debug and release, 32 bit and 64 bit, running on Windows 7, quad core processor):

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var obj = new object();
            var wRef = new WeakReference(obj);

            GC.Collect();
            obj = null;
            GC.Collect();

            Console.WriteLine(wRef.IsAlive); // Prints false.
            Console.ReadKey();
        }
    }
}

What happens when you try this code?

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • 2
    If `Should` is holding onto a reference, it would need to be in a static variable, otherwise it will have gone out of scope and be garbage collected. – Servy Mar 04 '13 at 16:22
  • Agreed, and that seems unlikely. But without seeing the source code for it, I have to assume the possibility. Although I think really it's just "undefined behaviour" with respect to the timing of when nulled-out references actually become garbage collectable, as you suggested. – Matthew Watson Mar 04 '13 at 16:28
  • The "Should" is from the FluentAssertions library. I get the same behavior using Assert.False instead. – TechnoTone Mar 04 '13 at 20:55
  • Thanks Matthew. Confirmed that this does work for me too. So the issue is really a result of the compiler optimizations. I wonder if it's possible to disable the relevant optimizations for my test so that I can make a true-to-life test scenario...? – TechnoTone Mar 04 '13 at 21:16
  • A minor, seemingly unrelated, tweak of your example code doesn't work though: http://stackoverflow.com/questions/34399139/garbage-collection-being-successful-seems-to-depend-on-non-related-things – chtenb Dec 21 '15 at 15:41
  • I have also seen the above code sample to work for .NET 4.5+ on my machine and not below. – Thulani Chivandikwa Feb 04 '16 at 17:03
0

I have a feeling that you need to call GC.WaitForPendingFinalizers() as I expect that week references are updated by the finalizers thread.

I had issues with the many years ago when writing a unit test and recall that WaitForPendingFinalizers() helped, so did making to calls to GC.Collect().

The software never leaked in real life, but writing a unit test to prove that the object was not kept alive was a lot harder then I hoped. (We had bugs in the past with our cache that did keep it alive.)

Ian Ringrose
  • 51,220
  • 55
  • 213
  • 317