How can I write a unit test to determine whether an object can be garbage collected?
Asked Answered
F

5

43

In relation to my previous question, I need to check whether a component that will be instantiated by Castle Windsor, can be garbage collected after my code has finished using it. I have tried the suggestion in the answers from the previous question, but it does not seem to work as expected, at least for my code. So I would like to write a unit test that tests whether a specific object instance can be garbage collected after some of my code has run.

Is that possible to do in a reliable way ?

EDIT

I currently have the following test based on Paul Stovell's answer, which succeeds:

     [TestMethod]
    public void ReleaseTest()
    {
        WindsorContainer container = new WindsorContainer();
        container.Kernel.ReleasePolicy = new NoTrackingReleasePolicy();
        container.AddComponentWithLifestyle<ReleaseTester>(LifestyleType.Transient);
        Assert.AreEqual(0, ReleaseTester.refCount);
        var weakRef = new WeakReference(container.Resolve<ReleaseTester>());
        Assert.AreEqual(1, ReleaseTester.refCount);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Assert.AreEqual(0, ReleaseTester.refCount, "Component not released");
    }

    private class ReleaseTester
    {
        public static int refCount = 0;

        public ReleaseTester()
        {
            refCount++;
        }

        ~ReleaseTester()
        {
            refCount--;
        }
    }

Am I right assuming that, based on the test above, I can conclude that Windsor will not leak memory when using the NoTrackingReleasePolicy ?

Farley answered 23/2, 2009 at 19:24 Comment(1)
We had a similar problem with Windsor in a production app and fixed it by using a similar policy. Windsor expects you to call "Release(component)" (which, if the component implements IDisposable, it cleans up for you), but there are some situations where that doesn't make sense.Purely
P
98

This is what I normally do:

[Test]
public void MyTest() 
{
    WeakReference reference;
    new Action(() => 
    {
        var service = new Service();
        // Do things with service that might cause a memory leak...

        reference = new WeakReference(service, true);
    })();

    // Service should have gone out of scope about now, 
    // so the garbage collector can clean it up
    GC.Collect();
    GC.WaitForPendingFinalizers();

    Assert.IsNull(reference.Target);
}

NB: There are very, very few times where you should call GC.Collect() in a production application. But testing for leaks is one example of where it's appropriate.

Purely answered 23/2, 2009 at 19:31 Comment(10)
Thanks. I understand that I generally should not call GC.Collect, I needed this to test parts of my code which I suspected was leaking memory.Farley
nice! is it possible to use a scope block instead of the anonymous delegate?Cheongsam
@Cheongsam I don't think that works - a scope block isn't the same in C# as in C++. The end of a scope block isn't enough to allow the memory to be GC'd - but you'd have to try it to be sure.Purely
To add to the bit about scope blocks. In a release build it might and might not release the strong reference within the block, depending on whether the actual jitted code re-uses the stack memory or register that holds the reference (more likely therefore if there are some new locals later on, though that's not the only factor). The correlation between scope and reachability isn't direct.Peerage
@PaulStovell - Looking at this it looks like great approach, but I can't seem to get it to work when testing it with a generic Windows Form. Am I missing something? Does this only work with service-type objects? Also I can't even get the compiler to take it without first initializing reference to null.Parthenogenesis
I'd love to use this, but I can't seem to get it to work in the happy case; that is, the WeakReference's target is still non-null after the collect even when WinDBG+SOS (!gcroot) tells me it has 0 roots. :(Congo
@PaulStovell Why do you need the construction inside the Action? Is there a reason why it is not enough to just set service=null before the GC? We do it like that and reference.Target returns always null, even in Release builds.Bristling
Instead of test for null, you also can use the IsAlive property. msdn.microsoft.com/en-us/library/…Doe
@PaulStovell I have seen a strange behaviour that if instead of Assert.IsNull(reference.Target); I use if(reference.Target != null) Console.WriteLine("Memory Leak"); I do see "Memory Leak" being printed and verified that service instance doesn't gets garbage collected in this case and hence the error. I even added GC.Collect(); after GC.WaitForPendingFinalizers(); This works only when this check if inside the Debug.Assert or Assert.Preadamite
Note if you want to use methods returning Tasks within the anonymous action you should not use async and await. Because if you use async Task all along the way the Task object will always have a reference to the action and to your object of interest. Use something like service.DoAsync().GetAwaiter().GetResult(); within the action instead.Hachman
W
5

Use dotMemory Unit framework (it's free)

[TestMethod]
public void ReleaseTest()
{
    // arrange
    WindsorContainer container = new WindsorContainer();
    container.Kernel.ReleasePolicy = new NoTrackingReleasePolicy();
    container.AddComponentWithLifestyle<ReleaseTester>(LifestyleType.Transient);
    var target = container.Resolve<ReleaseTester>()

    // act
    target = null;

    // assert        
    dotMemory.Check(memory =>
      Assert.AreEqual(
        0, 
        memory.GetObjects(where => where.Type.Is<ReleaseTester>().ObjectsCount, 
        "Component not released");
}
Wanitawanneeickel answered 28/10, 2015 at 9:16 Comment(2)
It is necessary to have resharper/dotMemory (licensed) to execute the test ?Audiophile
@Audiophile No, you don't have to have R# or dotMemory installed or have their licences. It's convinient to run tests under dotMemory Unit using R# test runner but you also can do it using free command line runner which you can download here jetbrains.com/dotmemory/unit The only case when you may need a standalone dotMemory if you would like to investigate a profile gathered but dotMemory Unit.Wanitawanneeickel
M
4

Perhaps you could hold a WeakReference to it and then check to see that it no longer alive (i.e., !IsAlive) after the tests have completed.

Mastoiditis answered 23/2, 2009 at 19:28 Comment(2)
That would only test if it HAS been garbage collected.Nb
@Ray: true, but the process would be to force a GC, wait for finalizers and then check IsAlive.Mastoiditis
C
4

Based on Paul's answer, I created a more reusable Assert method. Since string's are copied by value I added an explicit check for them. They can be collected by the garbage collector.

public static void IsGarbageCollected<TObject>( ref TObject @object )
    where TObject : class
{
    Action<TObject> emptyAction = o => { };
    IsGarbageCollected( ref @object, emptyAction );
}

public static void IsGarbageCollected<TObject>(
    ref TObject @object,
    Action<TObject> useObject )
    where TObject : class
{
    if ( typeof( TObject ) == typeof( string ) )
    {
        // Strings are copied by value, and don't leak anyhow.
        return;
    }

    int generation = GC.GetGeneration( @object );
    useObject( @object );
    WeakReference reference = new WeakReference( @object, true );
    @object = null;

    // The object should have gone out of scope about now, 
    // so the garbage collector can clean it up.
    GC.Collect( generation, GCCollectionMode.Forced );
    GC.WaitForPendingFinalizers();

    Assert.IsNull( reference.Target );
}

The following unit tests show the function is working in some common scenarios.

[TestMethod]
public void IsGarbageCollectedTest()
{
    // Empty object without any references which are held.
    object empty = new object();
    AssertHelper.IsGarbageCollected( ref empty );

    // Strings are copied by value, but are collectable!
    string @string = "";
    AssertHelper.IsGarbageCollected( ref @string );

    // Keep reference around.
    object hookedEvent = new object();
    #pragma warning disable 168
    object referenceCopy = hookedEvent;
    #pragma warning restore 168
    AssertHelper.ThrowsException<AssertFailedException>(
        () => AssertHelper.IsGarbageCollected( ref hookedEvent ) );
    GC.KeepAlive( referenceCopy );

    // Still attached as event.
    Publisher publisher = new Publisher();
    Subscriber subscriber = new Subscriber( publisher );
    AssertHelper.ThrowsException<AssertFailedException>(
        () => AssertHelper.IsGarbageCollected( ref subscriber ) );
    GC.KeepAlive( publisher );
}

Due to differences when using the Release configuration (I assume compiler optimizations), some of these unit tests would fail if GC.KeepAlive() were not to be called.

Complete source code (including some of the helper methods used) can be found in my library.

Candescent answered 22/3, 2012 at 17:24 Comment(12)
Does that work? I would have expected that @object is still referenced via the call stack of the calling method, so it shouldn't be collected.Purely
@PaulStovell I'm writing unit tests for it as we speak, but the code provided with object @object = new object() works. It doesn't work for I believe value types (and string). I'm guessing since the value is copied. I'll update if I can improve it. P.s. that didn't work on your version either.Candescent
@PaulStovell Works like a charm. I added an explicit test for string and added some common unit tests. Am I missing some cases?Candescent
Hi Steven, I see - I miss-read your code the first time. I didn't spot where you assign @object = null the first time I read it, but now it makes sense. Nice one!Purely
To make sure the tests all work in Release mode, you can make use of GC.KeepAlive (msdn.microsoft.com/en-us/library/system.gc.keepalive.aspx), to explicitly keep an object alive.Coin
Thank you for the tip @MattWarren, updated the unit tests accordingly.Candescent
"Strings are copied by value" - what did you mean by that?Academia
@Lou, You are right I might not have expressed it all that well. As per this answer, a better statement would be: "It's a reference type, but the reference is being passed by value." That is what I meant by "copied by value".Candescent
Ok, but aren't all references passed by value? :)Academia
@Academia Mmm, or maybe it had something to do that they are immutable. I am confused now. :) Sorry, I wrote this a long time ago, will need to look at this later. If you figure it out what I meant originally, feel free to update. :)Candescent
@StevenJeuris Thanks for the code - does this still work? I cant get it to work in VS2019, .NET Core 3.1 and C# 9.0. The Assert fails even if I just run IsGarbageCollected with a simple object.Kuehnel
I wrote this for .NET Framework 4.5, and seemingly never ported it to Core or Standard. So, not too surprising it doesn't work out of the box. I'm surprised it compiles out of the box. :)Candescent
C
1

This is not an answer, however you may want to try running your code in both Debug and Release modes (for comparison sake).

In my experience the Debug version of JIT'ed code is made easier to debug and thus may see references stay alive longer (I belive function scope) However, code JITed in Release mode may have the objects ready for collection quickly once it is out of scope and if a Collection happens.

Also not answering your question: :-)
I would be interested in seeing you debug this code using Visual Studio in Interop mode (Managed and Native) and then breaking after displaying a message box or something. Then you can open the Debug->Windows-Immediate and then type

load sos
(Change to thread 0)
!dso
!do <object>
!gcroot <object> (and look for any roots)

(or you can use Windbg as other's have posted in previous posts)

Thanks, Aaron

Confection answered 23/2, 2009 at 21:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.