Automatic memory management in .NET makes development a lot easier; however, it’s still easy to introduce memory leaks into your application. For example, in a complex application, it’s easy to forget to unregister event handlers, and these are notorious for holding on to objects which you don’t need to keep in memory any more. This typically leads to an increase in memory usage which, if it remains unchecked and unresolved, can lead to your application exhibiting poor performance, or even running out of memory and crashing. This is where a memory profiler becomes necessary.
ANTS Memory Profiler is a memory profiler for .NET applications – including ASP.NET web applications – that will help you locate memory leaks, investigate your application’s memory usage and perform health checks on your code. In essence, ANTS Memory Profiler will help you identify how to reduce your application’s memory footprint.
Let’s illustrate this point by using ANTS Memory Profiler 5 to locate a memory leak in a desktop application called QueryBee. It is a simple WinForms application for running queries against SQL Server databases. It is made up of a database connection dialog…
…and a query window to query the database.
We know our QueryBee application is leaking memory because, every time we open a query window and close it again, our memory usage keeps on increasing.
This is the profiling strategy we’re going to use:
- Wait for QueryBee to open.
- Take a first snapshot without using the application – this first snapshot will be used as a baseline.
- Interact with QueryBee – connect to database, enter a SQL query in the query form, and execute the query and close the form.
- Take a second snapshot.
- Examine the comparison the profiler shows us after it has finished taking and analyzing the second snapshot.
Let’s get started.
On opening up ANTS Memory Profiler, we are presented with a setup dialog (Fig. 3).
All we need to do is point it at QueryBee and click Start Profiling. The profiler starts up QueryBee and begins collecting performance counter data (Fig. 4).
At this point, we take a baseline snapshot by clicking on the Take Memory Snapshot button in the top-right corner. The profiler forces a full garbage collection and takes a snapshot of the heap memory it is using, and we get a first set of results (Fig. 5)
Now, we go back to QueryBee and perform the tasks which we think cause the memory leak: we get QueryBee from the system tray, we select a database, enter a SQL query in the query window, execute the query, and obtain some results.
Now that we’ve got the results, we close the form.
At this point, the window is gone, so we expect the memory usage to fall back to where it was around the first snapshot, but that is not the case.
So What’s Happening Here?
So what’s happening here? We take a second snapshot and get the results (see Fig. 8).
(We’re not planning on taking any further snapshots, so we click on the Stop Profiling button.)
We switch to the Class List to find out more. The Class List gives us a fuller picture of what’s in the snapshot.
We sort by Size Diff to see what has increased the most in size since our baseline.
We have 816 classes in our class list, so let’s use the Filter panel on our left to reduce the number of classes in the list. Because we have taken two snapshots, one very useful filter is the Comparing Snapshot filter. We only want to see new objects that have been created since the baseline, so we select the Only new objects filter.
We can see the impact of this filter on the bar above the class list – we’re left with 639 classes. We have already removed about a quarter of the classes. That’s a pretty good start.
Let’s reduce this further by applying another filter.
We know that we call Dispose() a number of times in our application. Memory leaks often occur when an object cannot be garbage collected, even if Dispose() has been called, because it is still being referenced by another object. So, let’s apply the filter Disposed objects which are still in memory.
That’s had a huge impact on the number of classes left. Scanning down the list, we can also see that there is an instance of our QueryForm class in the list.
That’s not right. We closed that form after the query had completed. To find out why it is still being held in memory, we’ll look at this instance of QueryForm, by clicking on the blue icon next to it.
Fig. 13: On its own, QueryForm is not that big, but the Size with Children column is showing that the one instance of QueryForm is holding on to over 80 MB.
We find that QueryForm is not that big, but the Size with Children column is showing that the one instance of QueryForm is holding on to a reasonable chunk of memory.
So, we know we’re leaking QueryForms, but we need to find out why. Let’s create an Object Retention Graph by clicking on the icon.
This Object Retention Graph shows what is still referencing our QueryForm. Once we figure this out, we’ll be able to go back into our code to break the chain of references that is keeping the QueryForm in memory.
There’s a handy hint in red telling us to start at the bottom and work our way up the graph until we find a reference that needs to be broken. We’ll just need to break the chain at one point to allow the garbage collector to clean up everything below that.
First, the graph is telling us that this System.EventHandler is referencing QueryForm and, if we step up one more level, it’s telling us that the event handler is referenced by our ConnectForm instance – this is the form that asked us for the database connection details. In other words, the ConnectForm is holding on to the QueryForm via an Event Handler.
If we look at this node more closely, we see that it’s actually being referenced by the ConnectForm‘s Foregrounded field.
Let’s find this Foregrounded event in our code. We right-click on the QueryBee.ConnectForm node and open the ConnectForm source code in Visual Studioâ¢.
The profiler automatically jumps to the Foregrounded event. We check where it is being used, by right-clicking on Find All References.
We’ve got three usages and we find that the last usage is where QueryForm registers for the Foregrounded event, but it doesn’t look like it unregisters. If we fix that, then the memory leak should go away.
The place to unregister that event is in the QueryForm‘s Dispose() method.
But since QueryForm doesn’t have a reference to the ConnectForm, we are going to have to store that in a member field.
Now we can modify Dispose() in the QueryForm.Designer.cs file.
We’re done, so we rebuild our application in Visual Studio.
So. Have we Really Fixed it?
Back in ANTS Memory Profiler, we start up a new profiling session. We want to find out that the reference to the QueryForm has disappeared.
Notice that it remembered our settings from last time, so all we need to do is click Start Profiling.
QueryBee opens up and we take a first snapshot to use as a baseline.
We connect to a database and execute a SQL query.
Now, we’ll take an extra snapshot, because we want to be able to verify that the query form has disappeared. This will give us results for snapshot 2.
Finally, we close the query window with the results grid and we take a third snapshot.
We switch to a comparison between snapshots 1 and 3, using the snapshot selection fields just under the timeline.
Let’s see if there’s a QueryForm still in the class list.
No, it’s gone. We’re no longer leaking the form.
As you saw, it was fairly easy to track down a form which was being leaked.
If you would like to try this on your own application, you can download a free 14-day trial from Red Gate’s website.