Finding Managed Memory leaks using the .Net CF Remote Performance Monitor
Service Pack 2 of the .Net Compact Framework V2.0 includes some new features in the Remote Performance Monitor aimed at finding memory leaks in the managed heap. These features allow you to take snapshots of the GC heap at any point in time and view the relationships between the live object instances in the heap. You can also compare multiple snapshots over time in order to spot allocation trends in your application as it executes.
In this post I'll walk you through the basic capabilities of the heap viewer features and provide an example to illustrate the type of problem the heap viewer is intended to solve.
But Managed Applications Can't Leak Memory, Right?
Because of the automatic memory management features in .Net it is tempting to believe that managed applications will never have memory leaks. While it is true that the garbage collector will always free objects that are no longer referenced, it is possible to hold on to an object longer than you intend to, thereby preventing it from being collected and essentially creating a leak.
Remember that the GC will not collect any object that is referenced by a "root" such as a static variable, a stack-based variable in the currently executing method and so on. So even if you're done using an object, if it is not detached from the root that is keeping it alive it won't be collected.
In the vast majority of cases, making sure an object is no longer referenced when you are done with it requires no additional work from the programmer. For example, when a method exits, all references held by the method's locals go out of scope and therefore are no longer attached to a root. However, there are a few relatively common cases in which more thought is required to make sure you're detaching an instance from its root(s). A few examples:
- Event handlers. Connecting an event handler defined in one object to an event defined by another object creates a dependency between those two objects. If you forget to detach the handler (i.e using -=) when you're done, it's easy to create a dependency that might cause the object in which the handler is defined to leak.
- Static variables. Consider a case in which you use a static variable to hold an array of objects. Every object in that array is rooted by the static. If you forget to explicitly remove an object from the array when you are done with it, it will not be collected until the static itself is.
More details on these scenarios, and examples of others, can be found by searching the web for something like ".Net memory leaks"
How do I know that I have a memory leak?
Memory leaks typically manifest themselves in obvious ways: your application starts seeing OutOfMemoryExceptions, performance degrades over time and so on. Your application likely has a leak if the amount of memory used by the GC continues to grow over time.
The best way to track the amount of memory used to store objects in the GC heap is to monitor the "Managed Bytes in use After GC" counter in RPM. Graphing the value of this counter over time using Perfmon is the easiest way to determine whether the heap is continually growing.
Capturing Snapshots of the GC Heap
Now that we've seen some simple examples of memory leaks, it's easy to imagine the data that would be useful in tracking leaks down:
- At any given point in time, which objects are alive in the GC heap?
- For each instance, what is the GC root that is causing it to stay alive?
- Over time, which types have an increasing number of instances?
The rest of this post describes how to use the heap snapshot feature in Remote Performance Monitor to gather this information.
Let's start by looking at how to use RPM to capture a snapshot of the heap. First, launch an application as you always have using the "Live Counters..." menu option (if you haven't used RPM before the following post will help get you started: https://blogs.msdn.com/stevenpr/archive/2006/04/17/577636.aspx ).
After your application launches, the "View GC Heap" button on the bottom toolstrip is enabled as shown in the following figure:
Each time you press "View GC Heap" a GC occurs and a new window will open to display the contents of the GC heap after the collection completes. I call this new window the "Roots" view.
The "Roots" View
The primary purpose of the roots view is to show the GC root that is causing each object instance to stay alive. Once you've narrowed down the type of instance that is causing the leak (more on how to do this later), you can use the roots view to determine why the instances in question aren't being collected.
The data displayed in the roots view is shown in the following three controls:
- The Statistics group
- The Types table
- The Root tree
The Statistics Group
The number of live objects in the heap, and the total size of those objects, is shown in the statistics group. When looking at multiple views, a quick glance at these statistics will tell you how the size of your heap is changing over time.
The Types table
The types table shows how many instances of each type are alive in the GC heap. The cumulative size of all instances of each type is also shown. Over time, the data displayed in this table will tell you whether the number of instances of a given type is growing or shrinking. Later, I describe how to use the "comparison view" to easily compare this data across multiple snapshots of the heap.
By default, the types table is sorted by number of instances. You can change the sort order by clicking on the table's column headers.
The check boxes next to the type names show which instances are displayed in the roots tree to the right. By default, the intent is to display the types that are defined by the application itself, not those defined by the .Net Compact Framework. Types with namespaces starting with "Microsoft" or "System" are excluded. You can change which types are shown in the roots tree by selecting the desired boxes and choosing the "Refresh Tree" button.
The Roots Tree
Each instance in the GC heap is displayed in the roots tree. Instances are grouped by type (unless there's only one instance of a given type, in which case that instance gets a top-level node of its own). The node for a given instance indicates the size in bytes of that instance along with a unique ID you can use to identify that instance throughout the tree.
By expanding the node for a given instance, you can walk your way up the reference hierarchy to see which root is causing that instance to stay alive. For example, consider the following subtree for an instance of type "Dice":
This tree shows us that our instance of Dice is referenced by an array of Dice. The array is then referenced by an instance of type Turn which is referenced by Form1. Form1 is a root as indicated by the word "root" and by the red color of the text.
The "References" View
The hierarchy shown in the roots view is "upside down" in that you're viewing the instances that reference the instance in question (the referents) . While this is the most useful view for finding leaks, it can also be useful to view the graph of objects a given instance references. Such reference graphs are provided by the "references view".
To view the reference graph for a given instance, right-click the instance in the roots tree and select "Display Object References" (this menu item is also available under the "View" pull down menu):
The reference view consists of a group that displays some general graph statistics and a tree showing the graph itself. The following screenshot displays the object graph for a main form. The reference graph for this particular form has 207 unique objects and is about 16K in size:
Comparing Snapshots of the GC Heap
You can determine which instances are leaking by taking multiple snapshots of the heap over time and comparing how many instances of each type are present. If the number of instances of a given type unexpectedly increases over time, it's likely that the instances are leaking. The "Comparison" view can help you spot these trends.
You can compare all open heap snapshots for a given application by selecting the "Compare Views..." item from the "View" menu on the roots view of any snapshot. The Comparison view looks like this:
The Comparison view shows instances counts for all types that appear in all open snapshots. Each row represents a type while each column shows the number of instances of that type that are present in a given snapshot. The snapshot columns are ordered by time to make it easy to spot trends.
The table is sorted based on the delta in the number of instances between snapshots. Those types that had the greatest change in the number of instances appear first, while those that didn't change at all are shown at the bottom. When the number of instances of a type changes from one snapshot to the next, a "+" or "-" sign is shown along with the delta.
Those rows that represent a delta are shown in red. Rows with no change in the number of instances are shown in black.
Identifying Leaks using the Comparison View
Memory leaks can be spotted by analyzing the rows in the table that are displayed in red. The types at the top of the table above are all from the .Net Compact Framework base class libraries. As I look at the list it's pretty clear that some sort of UI element is leaking. As I read down the list I eventually get to one of my own types: Players.Stats in this case. The number of instances of Players.Stats increased by 2 each time I took a snapshot. Players.Stats is a dialog box that displays basic statistical information about a player. Given that I create a new one of these each time a certain button is clicked, then let it go out of scope, I didn't suspect that instances of this type were leaking.
Now that I know which type is causing a leak, I can go back to the Roots view to see what GC root is causing my instances of Players.Stats to stay alive. By looking at the referents tree for Players.Stat I can see that my instances are staying alive because I've added an event handler that I've forgotten to "free". This event handler has created a dependency between a button on my main form and my Players.Stats dialog:
Armed with this data, I can go back into my code and detach my event handler using -= thereby breaking the dependency between my two forms and fixing my leak.
Once you've opened a snapshot you can save it to a file for later analysis. Choose the "Save" item from the File menu to save a snapshot. Choose the "Open-> .gclog file" item from the File menu to view a previously saved shapshot.
As always, please send any feedback my way...
This posting is provided "AS IS" with no warranties, and confers no rights.