One commonly misunderstood feature is the relationship between disposable objects (Objects that implement the IDisposable interface) and the GC. I know there are a lot of online resources about patterns and best practices but there is still a lot of confusion. I remember going to a Dev Lab to answer some questions by Whidbey early adopters, and being asked at what point in an object’s lifetime does the GC call Dispose. The answer: Never.
Dispose is just a method, like any other. Its purpose is allow deterministic resource cleanup, much like C++’s destructors, but without freeing the object’s memory. Like a C++ destructor, the developer puts whatever cleanup code is necessary to be performed when the object’s lifetime is up. Unlike a destructor, the object lives on after being "disposed of". This means an object may be in an invalid state after being disposed, and developers should consider using ObjectDisposedExceptions when methods in a disposed object are called.
What happens when you call Dispose()
Any cleanup your object need done in a timely fashion should be done in Dispose. Things like closing database connections, closing files, releasing bitmaps, etc. Unmanaged resources in particular should be released in Dispose. This is because the GC has no knowledge of anything not allocated on the managed heap. For example, a Bitmap object that encapsulates a 2MB image file reports only its managed size (the size of the managed object) to the GC. The GC knows nothing about the 2MB unmanaged image, thus will not collect it from memory. By calling Dispose, you tell the Bitmap object that you are finished with it, and it will release the image itself.
What doesn’t happen when you call Dispose()
- Calling Dispose does not prioritize the object for garbage collection. It simply unloads the object’s (unmanaged) resources from memory.
- Calling Dispose does not deallocate the object from memory. Only the GC does that when it performs a collection of the generation in which the object resides.
- The CLR does not insert or run any code not in Dispose. The behaviour of Dispose is defined by the developer.
- Dispose must be called explicitly by the application. It is never called by the runtime. The only exceptions are when using C#’s using statement (see ShawnFa’s article for a good explanation of what exactly goes on), and the foreach keyword in C# will result in Dispose being called on the Enumerator
- Dispose is NOT threadsafe. This means two threads can call Dispose on the same object at the same time. Like for any other synchronization-sensitive method, take steps to make sure this doesn’t happen. I’ll give an example of when this might happen in the next section.
What about the finalizer?
There are a few reasons why not to rely on the finalizer to clean up resources that I’ll cover in a future blog entry on finalizers. The main reasons you should be concerned with are performance and determinism. Finalizers are expensive to run, and there’s no order (or even guarantee) that they will be run (for example, the finalizer thread may time out, or be killed on AppDomain unload).
That being said, the finalizer should be your last chance to clean up resources, in case someone using your class forgets to dispose of it when done. A call to Dispose inside the finalizer reduces duplication of code. However, you want to make sure the resources don’t get released twice. In your Dispose method, you want to make sure you call GC.SuppressFinalize(this) after your clean up code. If you suppress the finalizer before the clean up, you’re limiting your ability to recover from failures during the cleanup.
Some of you may have noticed a race condition in my description above. Consider a situation where the call to Dispose is the last time your object is being referenced. As soon as Dispose is entered, the object is eligible for collection by the GC. Before that happens, the object’s finalizer gets called, which calls Dispose. You now have two threads inside Dispose, possibly double-freeing resources (this could be bad, as in the case of GCHandles). Make sure you follow the pattern I linked to above and use a disposing flag to avoid problems like this.
For more information about an object being collected while one of its methods are being run, check out these (very complete) blog posts by Chris Brumme: (Finalization and Lifetime, GC.KeepAlive, handle recycling).
If your .NET application is using unmanaged resources, make sure to implement the Dispose pattern, secure in the knowledge that nothing magical is going on.
EDIT: Added point about Dispose and foreach. Thanks Steve!
Edit: Minor corrections.