Using the Allocation Profiler to Detect Memory Leaks in .NET Applications

Symptoms

As developers, we are often faced with addressing memory leaks in our applications. Despite the fact that the .NET Framework includes automatic memory management, a number of memory allocation issues will remain in your application unless you are careful to avoid them.

A number of instances exist wherein the Garbage Collector in .NET fails to free allocated resources and thus create potential memory leaks. As such, it is critical to understand how Garbage Collection works and how to analyze your code and uncover any problem areas therein.

The article contains a step-by-step analysis of a known memory leak found in Microsoft .NET Framework v. 1.0.3705 via the Allocation Profiler included in AQtime 4. The problem appears when you use standard NumericUpDown and DomainUpDown controls in an application. The core of the problem lies with the Garbage Collector's inability to free instances of these classes as well as objects linked to them (for instance a form that contains one of these controls). As you can imagine, a form with a large number of such controls may cause significant memory leaks in any real-world application.


How to Locate Memory Leaks Using the Allocation Profiler

Creating a Test Application

First, we are going to create a test application that will consist of two forms: Form1 and Form2. Form2, contains NumericUpDown and DomainUpDown controls and is created from the main form (Form 1) by pressing the Show Form button. We'll place two controls (of each type) on Form 2. The source code for both forms is listed below (Example 1 and 2):

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel; using System.Windows.Forms;

using System.Data;

namespace WindowsApplication1
{
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.ComponentModel.Container components = null;

public Form1()
{
InitializeComponent();
}

protected override void Dispose(bool disposing)
{
if(disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

///

/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///









private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.SuspendLayout();
// // button1 //

this.button1.Location = new System.Drawing.Point(112, 96);
this.button1.Name = "button1";
this.button1.TabIndex = 0;
this.button1.Text = "Show Form2";
this.button1.Click += new System.EventHandler(this.button1_Click);
// // Form1 //

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(304, 254);
this.Controls.AddRange(new System.Windows.Forms.Control[] {this.button1});
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
}
#endregion


[STAThread]
static void Main()
{
Application.Run(new Form1());
}

private void button1_Click(object sender, System.EventArgs e)
{
Form2 frm = new Form2();
frm.Show();
}
}
}

Example 1: Form1.cs source code.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;

using System.Windows.Forms;
using Microsoft.Win32;

namespace WindowsApplication1
{
public class Form2 : System.Windows.Forms.Form
{
private System.Windows.Forms.DomainUpDown domainUpDown1;
private System.Windows.Forms.DomainUpDown domainUpDown2;
private System.Windows.Forms.NumericUpDown numericUpDown1;
private System.Windows.Forms.NumericUpDown numericUpDown2;
private System.ComponentModel.Container components = null;

public Form2()
{
InitializeComponent();
}

protected override void Dispose(bool disposing)
{
if(disposing)
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code
///


/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///



private void InitializeComponent()
{
this.domainUpDown1 = new System.Windows.Forms.DomainUpDown();
this.domainUpDown2 = new System.Windows.Forms.DomainUpDown();
this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
this.numericUpDown2 = new System.Windows.Forms.NumericUpDown();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown2)).BeginInit();

this.SuspendLayout();
// // domainUpDown1 //
this.domainUpDown1.Location = new System.Drawing.Point(168, 8);
this.domainUpDown1.Name = "domainUpDown1";
this.domainUpDown1.TabIndex = 0;
this.domainUpDown1.Text = "domainUpDown1";
this.domainUpDown1.SelectedItemChanged +=
new System.EventHandler(this.domainUpDown_SelectedItemChanged);
// // domainUpDown2 //
this.domainUpDown2.Location = new System.Drawing.Point(168, 40);
this.domainUpDown2.Name = "domainUpDown2";
this.domainUpDown2.TabIndex = 1;
this.domainUpDown2.Text = "domainUpDown2";
this.domainUpDown2.SelectedItemChanged +=
new System.EventHandler(this.domainUpDown_SelectedItemChanged);
// // numericUpDown1 //
this.numericUpDown1.Location = new System.Drawing.Point(8, 8);
this.numericUpDown1.Name = "numericUpDown1";
this.numericUpDown1.TabIndex = 3;
this.numericUpDown1.ValueChanged +=
new System.EventHandler(this.numericUpDown_ValueChanged);
// // numericUpDown2 //
this.numericUpDown2.Location = new System.Drawing.Point(8, 40);
this.numericUpDown2.Name = "numericUpDown2";
this.numericUpDown2.TabIndex = 4;
this.numericUpDown2.ValueChanged +=
new System.EventHandler(this.numericUpDown_ValueChanged);
// // Form2 //
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 118);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.numericUpDown2,
this.numericUpDown1,
this.domainUpDown2,
this.domainUpDown1});
this.Name = "Form2";
this.Text = "Form2";
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown2)).EndInit();
this.ResumeLayout(false);

}
#endregion


private void numericUpDown_ValueChanged(object sender, System.EventArgs e)
{
// Do something.
}

private void domainUpDown_SelectedItemChanged(object sender, System.EventArgs e)
{
// Do something.
}
}
}

Example 2: Form2.cs source code.

For each NumericUpDown and DomainUpDown control we define the event handlers used to interact with the actual controls. numericUpDown_ValueChanged() and domainUpDown_SelectedItemChanged().

With coding complete, we are ready to start our memory allocation tests via AQtime's Allocation Profiler.


Analyzing the Results

The first step in analyzing our results is to identify the problem. We'll first open the test application, WindowsApplication1.exe, in AQtime and select the Allocation Profiler. We'll set both Full Check and Profile Entire .NET Code options to By Classes. This will allow us to trace the usage of all objects created directly or indirectly (through a call stack) from the WindowsApplication1.exe methods.

Our test is rudimentary and involves executing the application, pressing the Show Form button, closing Form 2 and generating results via the Get Results command in AQtime's Run menu. Let's see the results we obtained by performing these steps:

Figure 1. Test results. The number of live instances of NumericUpDown is still 2.

When the Classes category is selected in the Explorer panel (see Figure 1), AQtime displays profiling results for classes whose instances were created during the profiler run. To view results for class instances, choose the Objects category. The following figures hold results shown by AQtime for class instances:

Figure 2. References to the instance of the Form2 class in the Call Graph panel.

Figure 3. References to the NumericUpDown instance in the Call Tree panel.

Analyzing the results (Fig. 1, 2, 3), we can see that the group of objects referenced by Form2 is still in memory, though explicit references to Form2 does not exist in the test application code. The only reference to the form, frm, was declared in the Form1.button1_click() method, but since the method has been executed, this reference is already out of scope and thus it has been released by the Garbage Collector. Hence, we come to conclusion that this object group is still referenced implicitly by one or more live objects.

This situation may take place because some root objects (such as global variables) still have direct or indirect references to some of these objects. This leads to memory leaks in the application because generally speaking, root objects have a lifetime equal to the application's run-time. A scenario which leads to potential leaks is demonstrated in Figure 4.

Figure 4. Potential Memory Leak. One or more root objects refer to a subgraph of the object reference graph.

The main complexity that appears when analyzing this situation is that a Root Object may refer indirectly to one or more intermediate objects (Object1, ...., ObjectN). This makes it difficult to find the reason behind the leak(s) (the Leak Objects link area) and thus eliminate them.

 
Let's perform a more complicated analysis of all external references to the objects of the Leak Objects subgraph to identify Root Object, Object1, ...., ObjectN.

To locate the problematic area, let's take a look at the reference tree in the Call Tree panel.

Figure 6. References to the NumericUpDown instance.

As you can see, references from a root object may go through incoming references to the instances of Form2, KeyEventHandler, KeyPressEventHandler, UpDownEventHandler and UserPreferenceChangedEventHandler classes. Else, the NumericUpDown object would become unreachable and would be deleted by GC. Figures 7a, 7b demonstrate this situation in detail.

a)

b)
Figure 7. GC functioning: a) Root Object and NumericUpDown refer to Live Object; NumericUpDown stays unreachable. b) Root Object refers to NumericUpDown through Live Object. NumericUpDown stays alive.

Having analyzed the tree of references in the above-mentioned objects, we can conclude that the root object (which is an instance of the System.Delegate[] class) refers to the UserPreferenceChangedEventHandler object indirectly (see Figure 8):

Figure 8. The root object refers to the UserPreferenceChangedEventHandler object.

 

Figure 9 shows a references graph from the System.Delegate[] root object to the NumericUpDown instance.

Figure 9. The reference graph: Delegate[] - UserPreferenceChangedEventHandler - NumericUpDown.

Now we are going to determine where the desired object of the UserPreferenceChangedEventHandler class was created. To do this, we will navigate to the Details panel and look at the Call Stack tab for this instance (Figure 10).

Figure 10. Call stack of the UserPreferenceChangedEventHandler instance allocation.

The call stack demonstrates that the UserPreferenceChangedEventHandler instance is created in the constructor of the UpDownBase class and it's added to the Microsoft.Win32.SystemEvents.UserPreferenceChanged event. Obviously, this handler is not removed from the given event which results in a leak. With this information, we can begin to address the allocation problem.

Solution: A Reflection-Based Workaround

There are several ways to solve this problem. For instance, we can inherit a new class from the NumericUpDown class and write the necessary finalization code in its Dispose() or Finalize() methods. A detailed analysis for such a workaround is beyond the scope of this article so we will describe the simplest method with which to solve the problem.

Since the UserPreferenceChanged() event handler is a private method of the UpDownBase class, we have no direct access to it. But we can do it via the Reflection API.

We'll make the following changes in the source code of Form2.cs (Example 3).

protected override void Dispose(bool disposing)
{
if(disposing)
{
if(components != null)
{
components.Dispose();
}
// Our correction starts here.
DisposeUpDown(domainUpDown1);
DisposeUpDown(domainUpDown2);
DisposeUpDown(numericUpDown1);
DisposeUpDown(numericUpDown2);
}
base.Dispose(disposing);
}


private void DisposeUpDown(UpDownBase obj)
{
SystemEvents.UserPreferenceChanged -=
(UserPreferenceChangedEventHandler)Delegate.CreateDelegate(
typeof(UserPreferenceChangedEventHandler), obj, "UserPreferenceChanged");
}

Example 3. Modified source code of Form2.cs.

The Form2.DisposeUpDown() method removes the UpDownBase.UserPreferenceChanged handler of the SystemEvents.UserPreferenceChanged event using Reflection. Thus, a reference to the root object is removed as well. Profiling results for the modified code are displayed in Figure 11.

Figure 11. Profiling results of the test case with the modified code. The results show us no live instances of NumericUpDown.

Conclusion

In this article we demonstrated how to detect memory leaks using AQtime ver. 4. With this information in hand, you can easily find existing memory leaks in your .NET application and thus eliminate them.

References

All mentioned trademarks are the property of their respective owners.