Although Microsoft will claim that it is "not possible to have a memory leak in managed code", 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:
public class Observable
{
public delegate void SomethingHappenedDelegate();
public event SomethingHappenedDelegate SomethingHappened;
// Rest of the class
}
public class Observer
{
private readonly Observable _observable;
public Observer(Observable observable)
{
_observable = observable;
_observable.SomethingHappened += UhOh_SomethingHappened;
}
void UhOh_SomethingHappened()
{
// Handle the event
}
}
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:
public void LeakAnObserver()
{
var observer = new Observer(_observable);
}
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).
public class Observer : IDisposable
{
// Existing code
public void Dispose()
{
_observable.SomethingHappened -= UhOh_SomethingHappened;
}
}
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:
public class LeakMonitor<T>
{
private readonly WeakReference _reference;
public LeakMonitor(T itemToWatch)
{
_reference = new WeakReference(itemToWatch);
}
public bool ItemIsAlive()
{
GC.Collect();
GC.WaitForPendingFinalizers();
return _reference.IsAlive;
}
public T Item
{
get
{
return (T)_reference.Target;
}
}
}
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!
[TestFixture]
public class MemTests
{
private Observable _observable;
[SetUp]
public virtual void SetUp()
{
_observable = new Observable();
}
[Test]
public void Test_That_Observer_Leaks()
{
var monitor = new LeakMonitor<Observer>(LeakMemory());
Assert.That(monitor.ItemIsAlive(), Is.True);
}
[Test]
public void Test_That_Disposing_Observer_Does_Not_Leak()
{
var monitor = new LeakMonitor<Observer>(LeakMemory());
monitor.Item.Dispose();
Assert.That(monitor.ItemIsAlive(), Is.False);
}
private Observer LeakMemory()
{
return new Observer(_observable);
}
}
6 comments:
Ok, but when Observable is garbage collected the Observer will not be referenced(because will not be present in the Application roots) and therefore will be GC too. So there should be no memory leaks [ I Hope :) ]
Right. But the point is that Observable might be around for a long time. Within Observable's lifetime, thousands of observers may have leaked. This is the case with memory leaks of this kind.
In an architecture that makes heavy use of the "Observable" pattern, these leaks happen all the time, unfortunately. Especially when the observable is a service that is kept alive for the lifetime of the application and the observer is not.
The tests show this from a unit level. Observable is kept alive during the extent of the test as a private field, where the observer is transient. The more real-world cases are with integration and functional tests.
This was a very useful blog post.
Thanks.
This isn't a memory leak.
Your code simply has the potential to use more memoray than it should.
It's not a leak, though - as the operating system and .Net will always know about the referenced memory.
Well, I guess it depends on what you consider a memory leak to be. If you use the C++ definition, which is roughly: "When you have allocated memory that you no longer have a reference to, so you can't delete it, you leak memory", then I agree.
But I am extending my definition of memory leak a bit further. I am saying "If you have allocated memory that you no longer know how to dereference, then you leak memory".
Imagine a case where the Observable is alive for the lifetime of the app. Nobody has a reference to the Observer anymore except for the Observable. The problem is that the Observable can't know what to release and what not to. In this case, you are stuck. That Observer object has no way of being garbage collected unless the Observable goes out of scope (Which won't happen if it is has the lifetime of the app) or if it releases all of it's events (which I can't think of a reason why it would ever do that).
So, again, if you disagree with my definition of a memory leak, then I can't disagree with your point. We just disagree with what we consider a memory leak to be. As far as I am concerned, if I have an app that is growing in memory size and I don't know why, then I am probably leaking somewhere, and I have to nip it in the bud.
This looks like a very useful post. I'll try the code concept you outline here for sure.
And I agree with you when defining what a "leak" is. Technically, as you rightly point out, what you're describing isn't, but I prefer the real-world definition. That's when your customer calls you up and says "your foobar service is sitting there showing 2Gb of ram in Process Explorer!". It's quite hard to explain to the customer that technically it isn't leaking because if you restart the service the memory is reclaimed. Telling the customer he should just restart the service every day isn't really a good option!
I can see this technique proving useful in that case. Many thanks.
Post a Comment