Thursday, February 28, 2008

The use of IDisposable and finalizers

An explanation of how the Garbage Collector handles IDisposable and finalizers.

At my work we have lots of code like this in our ASP.NET apps:
class LogonPage : IDisposable
{
    public LogonPage()
    {
    }
    
    ~LogonPage()
    {
        this.Dispose();
    }
    
    public void Dispose()
    {
    }
    
    // rest of code here
}

In other words, a class that implements IDisposable with an empty Dispose method and a finalizer (aka destructor) that calls Dispose.

The intention of this code appears to be to somehow give a "hint" to the Garbage Collector to free the memory associated with this object when the "destructor" is called.

Implementing a finalizer in this case has no benefit at all, in fact, it probably has the opposite effect of what was expected.  It will not cause the GC to free the object's memory any earilier than an object not implementing a finalizer and it places additional load on the GC.

There are two problems with the assumption that implementing IDisposable and a finalizer will somehow affect the GC to free the memory earlier:

1. C# finalizers are not deterministic;

2. Objects with finalizers are kept in memory longer than objects without and require additional GC processing.

Non-determistic finalization

It is a common misconception that a C# destructor is similar to a C++ destructor.  In C# the "~" method is often called a destructor, but that's a misnomer.  It's not a destructor in the C++ sense.  Instead the "~" method is really just an override for Object.Finalize().  It was a mistake of the .NET implementors to even use the "~" character to indicate a finalizer.

In C++ the destructor is deterministic, in other words, the developer is responsible for allocating and freeing memory and destructor is called at an exact predictable time: e.g. when an object on the stack reaches the end of its lifetime (i.e. when it leaves the scope of the code block) or when an object on the heap is explicitly freed with the delete keyword.

But .NET finalization is "non-deterministic".  That means you don't know when and in what order the GC will call the finalizers and you have no control over when the object will be freed.  Unlike with C++ destructors, the programmer has no way of explicitly calling a .NET finalizer.

Generations and finalization

The .NET GC uses a generational algorithm.  This means the GC divides the managed heap in three logical sections, or generations, called gen 0, gen 1 and gen 2.  Each of these generations have a maximum size, initially 256kb, 2MB and 10MB respectively. New objects are usually allocated on gen 0.

When a new object is allocated and the gen 0 memory is too full to accomodate the new object, the GC will reclaim the memory allocated to unreachable objects by compacting the gen 0 memory.  Next the GC promotes all gen 0 objects to gen 1.  This empties gen 0 for new objects.

Similarly when the gen 1 heap becomes full, the GC reclaims unused memory and promotes the gen 1 objects to gen 2.

The GC performs collection more frequently on lower generations than higher generations.

The GC frees finalizable objects much later than those wihout finalizers.

When a finalizable object is created, a reference to it is added to the "finalization list".  When the GC determines that an object is no longer reachable, but it's in the finalization list, it's not freed immediately.  Instead the finalizable object is put on another special queue called the "f-reachable queue" and promoted to the next generation.  A special thread called the "finalizer thread" monitors the f-reachable queue and calls the finalizers when necessary.  These objects will eventually be freed the next time a gen 1 or 2 garbage collection is performed.  Therefore it takes at least two garbage collection cycles to free finalizable objects.

Recommended IDispoable pattern

Implementing IDisposable.Dispose has no effect on the GC at all.  It's little more than just a standard interface for providing a "Close" method on your class to allow the user to close/unload resources.

Use the following "Dispose pattern" when your class uses other managed classes that implement IDisposable (note a finalizer is not needed here).  This is the most common scenario.
class MyClass : IDisposable 
{
    private bool disposed = false;
    private FileStream file;
    
    public MyClass()
    {
        file = new FileStream(...);
    }
    
    public void Dispose()
    {
        if (!disposed)
        {
            file.Close();
            disposed = true;
        }
    }
    
    public void Close()
    {
        Dispose();
    }
}

If you have to implement IDisposable and a finalizer, use the recommended pattern as described in MSDN.
class MyClass : IDisposable 
{
    private bool disposed = false;
    private IntPtr file = IntPtr.Zero; // Unmanaged resource
    
    public MyClass()
    {
        file = ...; // allocate unmanaged resource
    }
    
    ~MyClass()
    {
        Dispose(false);
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    public void Close()
    {
        Dispose();
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // dispose managed resources
            }
            if (file != IntPtr.Zero)
            {
                // free unmanaged resource
                file = IntPtr.Zero;
            }
            disposed = true;
        }
    }
}

Summary

Only implement IDisposable if you have a reason to do so, e.g. if your class has member fields that implement IDisposable so you need to provide a Dispose method call Dispose on the member fields.

The simplest guideline regarding the use of finalizers is: don't use it at all unless your class directly wraps unmanaged resources (such as a native file handle) and you want to make sure that Dispose gets called at some point, which really almost never happens in a typical ASP.NET application.  Don't create empty finalizers.

A class implementing IDisposable doesn't always require a finalizer, but a class that implements a finalizer should always implement IDisposable.

This was just my interpretation of the GC process.  For a more complete and accurate description, see Jeffrey Richter's book "CLR via C#".

Also see:
Post a Comment