IanG on Tap

Ian Griffiths in Weblog Form (RSS 2.0)

Blog Navigation

March (2014)

(1 item)

January (2014)

(2 items)

November (2013)

(2 items)

July (2013)

(4 items)

April (2013)

(1 item)

February (2013)

(6 items)

September (2011)

(2 items)

November (2010)

(4 items)

September (2010)

(1 item)

August (2010)

(4 items)

July (2010)

(2 items)

September (2009)

(1 item)

June (2009)

(1 item)

April (2009)

(1 item)

November (2008)

(1 item)

October (2008)

(1 item)

September (2008)

(1 item)

July (2008)

(1 item)

June (2008)

(1 item)

May (2008)

(2 items)

April (2008)

(2 items)

March (2008)

(5 items)

January (2008)

(3 items)

December (2007)

(1 item)

November (2007)

(1 item)

October (2007)

(1 item)

September (2007)

(3 items)

August (2007)

(1 item)

July (2007)

(1 item)

June (2007)

(2 items)

May (2007)

(8 items)

April (2007)

(2 items)

March (2007)

(7 items)

February (2007)

(2 items)

January (2007)

(2 items)

November (2006)

(1 item)

October (2006)

(2 items)

September (2006)

(1 item)

June (2006)

(2 items)

May (2006)

(4 items)

April (2006)

(1 item)

March (2006)

(5 items)

January (2006)

(1 item)

December (2005)

(3 items)

November (2005)

(2 items)

October (2005)

(2 items)

September (2005)

(8 items)

August (2005)

(7 items)

June (2005)

(3 items)

May (2005)

(7 items)

April (2005)

(6 items)

March (2005)

(1 item)

February (2005)

(2 items)

January (2005)

(5 items)

December (2004)

(5 items)

November (2004)

(7 items)

October (2004)

(3 items)

September (2004)

(7 items)

August (2004)

(16 items)

July (2004)

(10 items)

June (2004)

(27 items)

May (2004)

(15 items)

April (2004)

(15 items)

March (2004)

(13 items)

February (2004)

(16 items)

January (2004)

(15 items)

Blog Home

RSS 2.0

Writing

Programming C# 5.0

Programming WPF

Other Sites

Interact Software

Event Handlers, Circular References, and Alleged Memory Leaks

Wednesday 7 July, 2004, 04:43 PM

Philip Haack recently wrote about the fact that in .NET, delegates hold a reference to their target object, and can therefore cause the target to be kept alive. Once you've understood how delegates work, this is pretty straightforward, and unsurprising. The reason that people are often tripped up by this seems to be that they regard delegates as being opaque magic things, rather than thinking about how they work.

It's actually easy enough to build your own delegate-like object, which can help offer some insight into how the real thing behaves. Here's a home-made version of the standard System.EventHandler delegate type:

public class MyEventHandler
{
    private object target;
    private MethodInfo method;
    public MyEventHandler(object target, MethodInfo method)
    {
        this.target = target;

        this.method = method;
    }

    public void Invoke(object sender, EventArgs e)
    {
        object[] args = { sender, e };
        method.Invoke(target, args);
    }
}

It's missing a few frills, but this does provides the same core functionality as the standard EventHandler - it's capable of referring to a method with a particular signature on any object you like, and provides a means of invoking that method. Obviously it's missing a few features - it doesn't do multicast or asynchronous invocation. Also, it doesn't offer the compile-time type checking that C# does for real delegates. But the basic functionality is there, and with this example, it's pretty clear that this holds a reference to the object that it invokes methods on. So it seems equally clear that a real delegate will do much the same thing. (Although as mentioned previously, it is possible to use weak references to get away from this strong reference behaviour if you don't like it.)

This has implications for garbage collection: as long as something keeps a delegate reachable, that delegate will in turn keep its target reachable.

Because this isn't entirely obvious if you have never thought all that hard about what a delegate does, it often surprises people - not everyone expects objects that handle events to be kept alive for at least as long as the event source itself stays alive. This in turn gives rise to a popular misconception.

The Circular Reference Myth

In the comments of Philip Haack's article, Brad Wilson propagates the standard myth:

"Yes, delegates are a hard reference. Yes, your object will stay alive. In fact, this can cause of some circular reference memory leaks."

To be fair, the first two sentences are correct. It's the third one that is completely misleading - the leaks that you can cause in .NET by not understanding this issue have absolutely nothing to do with circular references.

Circular references don't cause objects to be kept alive. This is easy to demonstrate:

using System;

class HoldRef
{
    public object target;
    private string n;
    public HoldRef(string name) { n = name; }

    ~HoldRef()
    {
        Console.WriteLine("GC finalizing " + n);
    }
}

class App
{
    static void Main()
    {
        HoldRef first = new HoldRef("first");
        HoldRef second = new HoldRef("second");

        // Set up a circular reference
        first.target = second;
        second.target = first;

        // Let go of root references
        first = null;
        second = null;

        // GC
        GC.Collect();
        GC.WaitForPendingFinalizers();

        Console.WriteLine("Done GC");
        Console.ReadLine();
    }
}

This creates two objects that refer to each other, so we have a circular reference. It then removes any root references that were referring to the objects, and forces a garbage collect. I get this output:

GC finalizing second
GC finalizing first
Done GC

So as you can see, the presence of a circular reference is not sufficient to cause a leak in a garbage collected environment. (In a reference counting system like COM, circular references do cause leaks of course. This one of the main reasons .NET uses GC in the first place.)

Circular References and Event Handlers

It's only slightly more effort to prove that in the more specific case of a circular reference between a Form and an event handler, the circular reference still doesn't cause a leak. I wrote the following code:

public class Handler
{
    private static int lastID;
    public readonly int ID;
    private Form theTarget;

    public static Handler AttachWithCircularReference(Form f)
    {
        Handler h = new Handler(f);
        h.theTarget = f;
        return h;
    }

    public static Handler AttachWithUnidirectionalReference(Form f)
    {
        return new Handler(f);
    }

    private Handler(Form f)
    {
        ID = lastID++;
        f.Click += new EventHandler(f_Click);
        f.Text = ID.ToString();
    }

    private void f_Click(object sender, EventArgs e)
    {
        MessageBox.Show("Clicked", ID.ToString());
    }

    ~Handler()
    {
        MessageBox.Show("Handler GCed ", ID.ToString());
    }
}

Instances of this Handler class attach to the Click event of a Form. This will mean that the Form has a reference to a delegate holding a reference to the Handler. This means that if the Form is reachable (and therefore not collectable by the GC), the attached Handler will also remain reachable. Also, the Handler has a field, theTarget, which can optionally point back to the Form. I can create a Form and Handler with a circular reference between them like so:

Form f = new Form();
Handler.AttachWithCircularReference(f);
f.Show();

I've got an application with a main test form that runs that exact code when a button is clicked, and which has another button that forces a GC. If I run this and force a GC before I close the 'f' Form, then of course both the Handler and the Form remain alive. But if I close the Form and then force a GC, the Handler's finalizer runs. This is because closing a modeless Form causes Windows Forms not only to call Dispose on the Form, but also to cease to hold any references to the Form. (Windows Forms keeps forms reachable while they are open, but lets them go as soon as they close.)

So, there we have it. Proof that circular references between forms and their event handlers are not sufficient to cause memory leaks.

The Grain of Truth behind the Myth

So where does this myth come from? Well it's easy enough to cause a leak. We can simply cause the form to remain reachable. Consider this variation:

private ArrayList keptForms = new ArrayList();
private void btnNewKeep_Click(object sender, System.EventArgs e)
{
    Form f = new Form();
    Handler.AttachWithCircularReference(f);
    keptForms.Add(f);
    f.Show();
}

This is the code for another of the buttons on my test rig. This does more or less the same as before, but it dumps a reference to the Form in an ArrayList. That ArrayList is a member of my main form, so as long as my main form is open, the ArrayList will be reachable, as will everything the ArrayList. So now, I'm preventing the Form from being collected, which in turn also prevents the associated Handler from being collected.

This is the kind of scenario that has given rise to this myth that event handler circular references cause leaks. However, the circular reference is completely irrelevant here. Consider this example:

Form f = new Form();
Handler.AttachWithUnidirectionalReference(f);
keptForms.Add(f);
f.Show();

This creates a Handler which handles the Click event from the Form as before, but which does not hold a reference back to the Form. So there is no circular reference here. There's a chain: Windows Forms is keeping my main form reachable as long as it is open. The main form keeps the ArrayList reachable. That keeps the Form created in this code snippet reachable. That Form keeps the Handler reachable. There are no circular references, but this will keep the Handler alive. If that wasn't your intention, then you might regard this as being a memory leak.

So circular references do not cause memory leaks. And it is quite possible to leak memory without setting up a circular reference. Both of these statements are true regardless of whether the references are normal explicit object references, or references held via a delegate.

You can download the code I'm using to test all this from here.

Copyright © 2002-2013, Interact Software Ltd. Content by Ian Griffiths. Please direct all Web site inquiries to webmaster@interact-sw.co.uk