Garbage-Collection, Finalizers and Dispose: What Every C# Programmer Should Know
Because C# is garbage-collected, I don't need to worry about cleaning anything up. Not.
If you've ever wondered why your C# application is so bloated, this post is for you.
The garbage collector in .NET ensures that you don't have to worry about freeing managed memory that you allocated. So, you can allocate strings, arrays, hash tables and the like all day long without incurring a huge memory cost. But you still need to worry about cleaning up if you're doing anything that involves resources outside .NET. So, if you opened a file, you need to close it. If you created a window, you need to destroy it. And so forth.
Now in fact, if you don't clean these things up, eventually the garbage collector will do it for you. But there's a catch. The System.Drawing.Image object, for example, has very little state inside it except for a handle to the unmanaged image. The garbage collector will think that collecting this object will only get a few bytes back, but collecting it may well result in the release of a megabyte of unmanaged memory. So you can leak lots and lots of Images, Fonts, Icons, Brushes, Regions and whatnot before the garbage collector kicks in. Meanwhile, your process has swollen to occupy many megabytes of memory.
When developing classes in .NET, you need to have two relevant goals: achieving timely cleanup in well-written programs, and preventing permanent leaking of unmanaged objects even in poorly-written code. The Dispose() method is the key to the first, and finalizers are the key to the second.
In order to understand more deeply, let's start by looking at what .NET guarantees:
The Guarantees
The .NET runtime makes you three guarantees here:
- As long as there is any conceivable way for your object to be referenced, its memory will remain allocated. So you don't have to worry about dealing with stale references to unallocated objects. This is goodness. In determining whether your object might be referenced, the system considers the values of all global (static) variables, and the values of all local variables on the stack of every thread in the process.
- Some arbitrary time after it becomes totally impossible to ever reference an object again, the .NET runtime will reclaim that object's memory to use for other purposes. This might happen the same microsecond that the object becomes unreachable, and might happen many minutes later. But if your process lives long enough, it will happen.
- If your object has a finalizer, then some time after it first becomes unreachable but before its memory is deallocated, the system will call its finalizer routine.
If an object has a finalizer that hasn't been run yet, you can opt out of the third guarantee and prevent the finalizer from running by calling GC.SuppressFinalize. Similarly, you can make an object's finalizer run a second time by calling GC.ReRegisterForFinalize, but you're twisted if you do so.
IDisposable and Dispose()
There is an interface System.IDisposable, with a single method: void Dispose(). Unlike finalizers, all use of Dispose is strictly conventional. But it's a convention like "drive on the right side of the road," not a convention like "don't wear white shoes after Labor Day." So you had better understand and abide by it.
If you write an object that needs some cleanup when your client is done, implement IDisposable and code up a Dispose method. And if you create an instance of someone else's object that implements IDisposable, you should call its Dispose method when you're done.
Again, it's just a convention. But you violate it at your peril.
Your Mission
Remember your goals: timely cleanup, and no leaks.
To achieve the first goal, whenever you create an instance of a class that implements IDisposable, you must ensure that its Dispose method gets called in a timely fashion. There are two principal ways of doing this:
- If it's only needed for the life of one method, use a using statement:
using (MyClass myObject = new MyClass(...))
{
// your code here
}
This using statement is just syntactic sugar for "call Dispose on the way out". The Dispose method is called automatically when control hits the closing brace, or when control leaves the block in some unusual method (return, exception, goto, whatever). The using statement is exactly equivalent to coding a try ... finally statement, but less wordy.
- If the disposable object is needed for the lifetime of one of your objects, then implement IDisposable yourself, and have your Dispose method call Dispose on the object you created. Note that in doing this, we've pushed the need to call Dispose up onto the shoulders of whoever created your object.
To achieve the second goal, whenever you create a class that directly wraps an unmanaged resource, create a finalizer method that releases the resource.
FxCop's Advise: The Dispose Pattern
FxCop is a wonderful tool; it can find all kinds of subtle bugs just by analyzing your code, even before you've ever run it. Here's its advise about coding classes that need cleanup behavior:
- Inherit from IDisposable.
- Implement IDisposable's Dispose method exactly as follows:
public void Dispose()
{
Dispose (true);
GC.SuppressFinalize (this);
}
- Implement a finalizer method exactly as follows:
public ~YourClassName()
{
Dispose (false);
}
- Implement the following method, which is also named Dispose but has no relationship to IDisposable's Dispose:
public virtual void Dispose (bool disposing)
{
if (disposing)
{
// do managed cleanup here, primarily by
// calling other objects' Dispose method.
}
// do unmanaged cleanup here,
// using P/Invokes as necessary
}
(The code is slightly different if you're inheriting from a class that implements IDispose.)
This code strikes me as overkill. It assumes that the same class might both wrap unmanaged resources, hold references to other disposable objects, and have its own cleanup to do. It also assumes that the class might have sub- or super-classes that also have their own cleanup responsibilities.
Most of the time, however, you won't directly hold unmanaged resources, and won't have any of your own cleanup to do. So if you follow FxCop's advise, you'll write a finalizer that does nothing, and a Dispose method that keeps the finalizer from executing. I don't care very much about the execution time of this fancy no-op, but I hate wasting my brain cells writing and reading this crap. So here are my simplified rules:
Jimbo's Rules of Cleanup
- If you need to directly wrap an unmanaged resource, then write a class that exactly wraps one resource. If you always use two different resources together, wrap each in a separate class.
- If you're wrapping an unmanaged resource that's represented by something the same size as a pointer (usually a native pointer or handle), make your wrapper class inherit from SafeHandle (or one of its subclasses), and override its ReleaseHandle method. This covers more than 90% of all native resource wrappers.
- If you're wrapping an unmanaged resource that's of a different size than a pointer, wrap it in a class that implements IDisposable and has the following methods:
public void Dispose()
{
if (resourceIsntAlreadyCleanedUp)
{
CleanUpTheResource;
FlagResourceAsCleanedUp;
}
GC.SuppressFinalize(this);
}
public void ~YourClassName()
{
if (resourceIsntAlreadyCLeanedUp)
{
CleanUpTheResource;
FlagResourceAsCleanedUp;
}
}
If this rule doesn't apply, don't code a finalizer method.
- If your class owns other objects that implement IDisposable, then your class must implement IDisposable, and your Dispose method must call the other objects' Dispose methods.
- If you need other cleanup behavior (flushing a buffer, for example), implement IDisposable, and have your Dispose method perform your cleanup before calling other objects' Dispose methods.
- If you're inheriting from a class that implemented the full disposable pattern, or if you intend that others inherit from you, use the full disposable pattern.
- The only good use for GC.SuppressFinalize is as in rule 3 above. There are no good uses for GC.ReRegisterForFInalization. And if you ever call either of these routines with a parameter other than 'this', you deserve a wedgie.
Postscript
And while I'm at it, I've often seen attempts to repair the absence of calling Dispose by sprinkling the following throughout the program:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Don't do this. Besides being horribly expensive, it's ugly. It's a lot like driving to the emergency room of your local hospital every afternoon and getting a tourniquet applied to your leg, on the off chance that you shot yourself in the foot.