Udostępnij za pośrednictwem


Automated leak detection in managed code

Managed code makes memory management much easier, but it's still possible to have unintended memory leaks. Here's an example. In Max, we have a sign in control on our home page. This control register's an event handler with an object that manages the sign in state and then changes its style when the sign in state changes. When you close the main window, we keep the application running in the system tray, and you can open a new window via the system tray. We had a bug where we weren't unregistering from the event handler, so the object that manages sign in state was keeping the sign in UI control alive. This in turn was keeping most of the rest of the UI alive. The solution was to register for the Unloaded event on this Avalon element and unregister from the event handler when the Unloaded event is fired.

In all major native applications I've worked on in the past, there's been some mechanism for automatically detecting leaks. One common practice is to have a custom allocator/deallocator that tracks all allocated memory. Then, when the application quits, if any memory wasn't freed, it asserts that there was a leak. This requires freeing singleton type objects that you might not otherwise free, but it's really important to find leaks as soon as possible. As with all bugs, the sooner you find it, the cheaper it is to fix.

Unfortunately, it's not as straightforward to detect managed leaks. With native code, a leak is well defined. It's memory that's no longer referenced, but hasn't been freed. That type of leak is impossible with managed code. A managed leak is keeping a reference to something that you don't need.

Here's a solution we are using to automatically detect managed leaks. Unfortunately, it's not incredibly general, but maybe it will give you ideas in your application.

First, we have the following helper class:

internal class LeakChecker

{

    public LeakChecker()

    {

        _objectCount++;

    }

    ~LeakChecker()

    {

        _objectCount--;

    }

    public static void AssertOnLeaks()

    {

        Debug.Assert(_objectCount == 0, "Leaks detected");

    }

    private static int _objectCount = 0;

}

This class increments a static int in its constructor and decrements it in its finalizer. Then, in our debug build, we add a LeakChecker object to several of our UI elements. Then, (again in our debug build only), when closing the main window, we force garbage collection and then call LeakChecker.AssertOnLeaks(). To force garbage collection, do the following:

GC.Collect(GC.MaxGeneration);

GC.WaitForPendingFinalizers();

This will block until garbage collection is done and all fnalizers are run. At this point, if all of the UI objects have been properly freed, there shouldn't be any LeakChecker objects around anymore. So, the assertion should not fire. But, if there's a leak, you'll get an assert when closing the window, so you can find the leak quickly.

This method is far from perfect. For example, if there's an animation going on in the window, the animation system may keep the UI items alive a bit longer. So, you may get some false positives. The solution requires having a time period when you know no objects of some class should exist. But, it's been working pretty well for us so far. Hopefully this will give you some ideas for leak checking in your managed app.

BTW, once you find that you have a leak, you'll need to track it down. Rico Mariani has a great post on using windbg/cdb/ntsd to track them down. I've also been really impressed with .NET Memory Profiler.

Comments

  • Anonymous
    November 08, 2005
    To force a GC you must call

    GC.Collect(GC.MaxGeneration);
    GC.WaitForPendingFinalizers();
    GC.Collect(GC.MaxGeneration);

    (note the last GC.Collect...)
    The last call is necessary, because "WaitForPendingFinalizers" will not remove the object from the GC...