Writing Tests to Catch Memory Leaks in .NET

kick it on DotNetKicks.com

Although Microsoft will claim that it is, most seasoned.NET developers will laugh at that statement.  It turns out that it is very easy to leak memory -- just keep a referencing object around longer than the referenced object, and you can leak.  There at least two tools on the market that are designed specifically to seek out memory leaks of this kind (Scitech and ANTS).

The most common case of this happens with events in C#.  Take the following example:

1: public class Observable

2: {

3: public delegate void SomethingHappenedDelegate;

4: public event SomethingHappenedDelegate SomethingHappened;

5: 

6: // Rest of the class

7: }

1: public class Observer

2: {

3: private readonly Observable _observable;

4: 

5: public Observer(Observable observable)

6: {

7: _observable = observable;

8: _observable.SomethingHappened += UhOh_SomethingHappened;

9: }

10: 

11: void UhOh_SomethingHappened

12: {

13: // Handle the event

14: }

15: }

In this example, the Observer class will hook the event on the Observable class during construction.  Because of the way that events work in C#, the Observable object has a reference to the Observer.  In the following example, the Observer will be alive (at least) as long as the Observable class is alive.  For this reason, the following method will cause a memory leak:

1: public void LeakAnObserver

2: {

3: var observer = new Observer(_observable);

4: }

In most cases, the Observer instance would be garbage collected as it went out of scope.  Instead, since it is kept alive through the event handler of the Observable, we leak memory.

There is a pretty easy way to solve this.  Simply unhook the event in the disposal event (actual "Dispose Pattern" removed for brevity).

1: public class Observer: IDisposable

2: {

3: // Existing code

4: 

5: public void Dispose

6: {

7: _observable.SomethingHappened -= UhOh_SomethingHappened;

8: }

9: }

Great!  Now, as long as we dispose the Observer, all references will be removed and the object will get garbage collected.  Unfortunately, it is VERY easy to forget to call the Dispose method.  I want to write some tests to make sure that these objects are garbage collected.

This is a tall order to fill.  Having a reference to the object will cause it to stay alive.  How do you ask an object if it is alive without actually having a reference to the object?  This is where the WeakReference class comes in.  It is a magical class that keeps a reference to an object without the garbage collector knowing about the reference.  I wrote the following class to help me monitor and test if it still alive:

1: public class LeakMonitor

2: {

3: private readonly WeakReference _reference;

4: 

5: public LeakMonitor(T itemToWatch)

6: {

7: _reference = new WeakReference(itemToWatch);

8: }

9: 

10: public bool ItemIsAlive

11: {

12: GC.Collect;

13: GC.WaitForPendingFinalizers;

14: 

15: return _reference.IsAlive;

16: }

17: 

18: public T Item

19: {

20: get

21: {

22: return (T)_reference.Target;

23: }

24: }

25: }

Here are two examples of tests that illustrate the use of LeakMonitor.  These are over-simplified unit test examples for this blog post, but you can see how this can be extended to integration and functional tests to verify that inner objects are not leaked.  Be creative!

1: [TestFixture]

2: public class MemTests

3: {

4: private Observable _observable;

5: 

6: [SetUp]

7: public virtual void SetUp

8: {

9: _observable = new Observable;

10: }

11: 

12: [Test]

13: public void Test_That_Observer_Leaks

14: {

15: var monitor = new LeakMonitor(LeakMemory);

16: 

17: Assert.That(monitor.ItemIsAlive, Is.True);

18: }

19: 

20: [Test]

21: public void Test_That_Disposing_Observer_Does_Not_Leak

22: {

23: var monitor = new LeakMonitor(LeakMemory);

24: monitor.Item.Dispose;

25: 

26: Assert.That(monitor.ItemIsAlive, Is.False);

27: }

28: 

29: private Observer LeakMemory

30: {

31: return new Observer(_observable);

32: }

33: }

This article is part of the GWB Archives. Original Author: Brian Genisio's House Of Bilz

New on Geeks with Blogs

  • We Won The One Award I Actually Care About

    Full Scale made the Inc. 5000 for the fifth year straight, the 12th listing across my three companies. Here is why the one award you cannot buy is worth stopping for.

  • Your Customers Build the Features Now

    I let a tool I liked sit dead for a year rather than build the features I wanted. An MCP server meant I never had to, and your customers can do the same to your product.

  • Get the Size of a Directory in Linux the Easy Way

    du -sh for the quick answer, ncdu for the cleanup, df for the disk itself: every command for checking directory size in Linux, plus why du and df never agree.

  • Vim Search and Replace: The Ultimate Guide

    One :%s command replaces every match in a file before a find dialog would even open. The Vim substitute patterns worth the muscle memory: flags, ranges, capture groups, and multi-file edits.