views:

298

answers:

3

There are two ways (that I know of) to cause an unintentional memory leak in C#:

  1. Not disposing of resources that implement IDisposable
  2. Referencing and de-referencing events incorrectly.

I don't really understand the second point. If the source object has a longer lifetime than the listener, and the listener doesn't need the events anymore when there are no other references to it, using normal .NET events causes a memory leak: the source object holds listener objects in memory that should be garbage collected.

Can you explain how events can cause memory leaks with code in C#, and how I can code to get around it using Weak References and without Weak References?

+8  A: 

When a listener attaches an event listener to an event, the source object will get a reference to the listener object. This means that the listener cannot be collected by the garbage collector until either the event handler is detached, or the source object is collected.

Consider the following classes:

class Source
{
    public event EventHandler SomeEvent;
}

class Listener
{
    public Listener(Source source)
    {
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        source.SomeEvent += new EventHandler(source_SomeEvent);
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }
}

...and then the following code:

Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;

Even though we assign null to listener, it will not be eligible for garbage collection, since newSource is still holding a reference to the event handler (Listener.source_SomeEvent). To fix this kind of leak, it is important to always detach event listeners when they are no longer needed.

The above sample is written to focus on the problem with the leak. In order to fix that code, the easiest will perhaps be to let Listener hold on to a reference to Source, so that it can later detach the event listener:

class Listener
{
    private Source _source;
    public Listener(Source source)
    {
        _source = source;
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        _source.SomeEvent += source_SomeEvent;
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }

    public void Close()
    {
        if (_source != null)
        {
            // detach event handler
            _source.SomeEvent -= source_SomeEvent;
            _source = null;
        }
    }
}

Then the calling code can signal that it is done using the object, which will remove the reference that Source has to ´Listener`;

Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;
Fredrik Mörk
*I don't think this is [entirely] true.* Cyclic references within the CLR proper should take care of themselves (they can be detected). The real problem is when dealing across runtimes -- COM interop is a big case here. This is because the CLR can not adequately "see" into the COM (non-CLR) runtime/host and thus can not detect and clean-up cyclic references. I am not sure how this affects some standard .NET components such as WinForms (which run to native), though.
pst
@pst: there is no cyclic reference; note that `Listener` holds no reference to `Source`. `Listener` is being kept alive because `Source` is holding a reference to `Listener`. As long as something holds a reference to `Source`, `listener` cannot be collected, but `Listener` is not preventing `Source` from being collected.
Fredrik Mörk
You should edit your post to tell people how to dereference event handlers and *where* to do it.
George Stocker
+1 Great description of what's going on Fredrik. It obviously gets a real problem when the `Listener` classes are created and subscribe to `Source` either on a certain event or in some sort of loop, making it happen a lot of times.
Sander Rijken
@pst, This is not an issue of cyclic references but rather an issue of object lifetime. The example is not great, but it does seem to try show that the event source out lives the listener, in which case the listener will not be collected until the event source no longer has references or the listener explicitly removes it's event handler from the event source. A typical Windows Form would listen to events of it's child controls, in which case the event source has a shorter lifetime and the Form so no memory leak in this typical WinForms scenario, other scenarios exist and often bite.
Chris Taylor
ty this really helped me :) Just one mroe question if we have just used ordinary "-=" before assigning null to listener would memory leak occur
jnvjgt
@Fredrik Mörk Great explanation. I was thinking of asking the same question like listner = null scenario you explained. Once agina great!
Avatar
@jnvjgt @Avatar No, it would not. Assigning `null` to the listener doesn't really do anything here, other than tell the Garbage collector the item is ready to be collected; which it would do once it left the block anyway. It's seldom necessary to set an object to null to get it to be collected once it leaves the block, but some programmers put it there to explicitly state that they `null`'d the object.
George Stocker
A: 

There are, strictly speaking, no "memory leaks" within the "sandbox" of a managed .NET project; there are only references held longer than the developer would think necessary. Fredrik has the right of it; when you attach a handler to an event, because the handler is usually an instance method (requiring the instance), the instance of the class containing the listener stays in memory as long as this reference is maintained. If the listener instance contains references to other classes in turn (e.g. backreferences to containing objects), the heap can stay quite large long after the listener has gone out of all other scopes.

Maybe someone with a bit more esoteric knowledge of Delegate and MulticastDelegate can shed some light on this. The way I see it, a true leak COULD be possible if all of the following were true:

  • The event listener requires external/unmanaged resources to be released by implementing IDisposable, but it either does not, or
  • The event multicast delegate does NOT call the Dispose() methods from its overridden Finalize() method, and
  • The class containing the event does not call Dispose() on each Target of the delegate through its own IDisposable implementation, or in Finalize().

I've never heard of any best practice involving calling Dispose() on delegate Targets, much less event listeners, so I can only assume the .NET developers knew what they were doing in this case. If this is true, and the MulticastDelegate behind an event tries to properly dispose of listeners, then all that is necessary is proper implementation of IDisposable on a listening class that requires disposal.

KeithS
+1  A: 

Read Jon Skeet's excellent article on events. It's not a true "memory leak" in the classic sense, but more of a held reference that hasn't been disconnected. So always remember to -= an event handler that you += at a previous point and you should be golden.

Jesse C. Slicer