This is something that often bites new developers. “There can be no memory leaks in a garbage collected runtime”. Well, perhaps not in theory, but in real life under the wrong circumstances there will.
Ok, not memory leaks in the terms original meaning, unreferenced memory, but memory you thought you got rid of but it hangs around never the less.
In my experience it mostly happens when we use either Events or Timers. The scenario for events is likely a view-driven application where we pop views in and out of existence. During the lifetime of the view it likely have to respond to events from the host window. Events like “the user clicked the save-button” or similar.
So, during initialization of the view it hooks up to the ev_Save-event in the host. Later, when the user switch view, you drop the reference to the old view and replace it with another one. Gone. Right?
No, the view you just disposed clings on for dear life to the event, and is not eligable for garbage collection.
I have a class representing the view called Worker. I simulate adding 10000 views and then print out the memory consumption.
for (int a = 0; a < 10000; a++) | |
{ | |
new Worker(this); | |
} | |
memoryLabel.Text = "Memory consumption:" + GC.GetTotalMemory(true)/1024; |
Note that I’m not saving any references to it. Much like just adding it to the current view. I’m passing in a reference to the hosting window which the client uses to hook up all of the events for interacting with the user.
The constructor of the client “view” just hooks up the fake “save”-event. The heavy byte array is just to make the leak more visible in Task Manager.
private readonly byte[] bLoad = new byte[99999]; | |
HostWindow _host; | |
public Worker(HostWindow host) | |
{ | |
_host = host; | |
host.ev_Click += HostEventTriggered; | |
} |
When I press the button invoking the “save”-event I can see that my array of listeners contains all the 10000 objects.
private void TriggerChildObjects(object sender, EventArgs e) | |
{ | |
countLabel.Text = "InvocationList contains " + | |
(ev_Click == null?0:ev_Click.GetInvocationList().Length) + " objects"; | |
} |
Remember I didn’t keep any references to the clients. But rather, the client kept a reference to the host.
Look at this amazing piece of software 🙂
Anyway, to my surprice I hit 10000 save-events instead of the one on the screen.
The easiest way to mitigate this is to make sure the client unsubscribes to all events before you loose it. The perhaps cleanest way to do this is to implement the IDisposable interface and then, during the view-switching, invoke the Dispose()-method.
I simulate this in my handler for the “Dispose”-button
private void Dispose(object sender, EventArgs e) | |
{ | |
if (ev_Click == null) | |
return; | |
foreach (var w in ev_Click.GetInvocationList()) | |
{ | |
using (var x = w.Target as IDisposable) | |
{ | |
x.Dispose(); | |
} | |
} | |
countLabel.Text = "InvocationList contains " + (ev_Click == null ? 0 : ev_Click.GetInvocationList().Length) + " objects"; | |
memoryLabel.Text = "Memory consumption:" + GC.GetTotalMemory(true) / 1024; | |
} |
A comment on calling GC.GetTotalMemory(true). When you pass true, the runtime will perform a full GC before returning the memory numbers.
Also, you will not get all that memory back. I.e. it will not drop to its original size. The application will keep the allocation, but regard it as usable. So when you click on allocate again after pressing Dispose, you won’t get an OutOfMemory exception. This is just the way .NET works
This scenario is, as I mentioned above, very common in Silverlight- and WinForm applications. Perhaps you are using MEF or Jounce or any other helper library that makes the tedious view plumbing go away. But it might also make you think that all this is automagically taken care of.
It is not.
Sample project here