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 AutomatedQA's
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.