Object Lifetime and Destruction in .NET
I've noticed lately that a lot of people seem to have a kind of vague perception of what happens to objects after you're finished using them in .NET. It's kind of critical to understand exactly how the lifetime model works, because it's much more complex than older languages like C++ and can have important implications for an application. The relevant part of the standard is in section 10.9 Automatic Memory Management. There are three steps that can happen at the end of the lifetime of an object in C#:
1) Disposal (if an IDisposable object)
2) Finalization (the "destructor")
3) Collection (end of physical lifetime)
Step 1 is a fully explicit - dare I say, design pattern - which we use to create deterministic logical (but not physical!) lifetimes for objects. There is no magic behind it (with the exception of the using statement); calls to Dispose are explicit and the runtime injects no special behavior. The common Dispose pattern is not mandatory and again has no magic behind it. If you look closely at this pattern, there are two interesting behaviors. First, the call to GC.SuppressFinalize, which prevents Step 2 from happening, and second, the fact that the finalizer will dispose the object.
Step 2 is the first bit of behind the scenes magic. The C# finalizer looks like a C++ destructor, but it doesn't really behave like one. When an object is no longer accessible, it is marked for finalization. Then another separate, low priority finalizer thread goes through all of the objects marked for finalization, calling their finalizers. An object can be revived by its own finalizer if it makes itself accessible again*. The memory of the object has not been affected in any way at this point. Also keep in mind that finalization, unlike disposal, is completely non deterministic and is dependent on what the underlying runtime decides to do. Particularly important is the fact that what order classes are finalized in is not specified -- and as a result, it is not safe to access any of your reference type members from the finalizer, since they may have already been finalized.
* An object which revives itself during finalization is still marked as finalized, and the finalizer will not be called again when the object becomes inaccessible again. Use GC.ReRegisterForFinalize to make the runtime call the finalizer again.
Step 3 is where the memory of the object is finally destroyed and released back to the OS. This is where the pointer to the top of the managed heap is moved internally and all that fun stuff. An object becomes eligible for collection if, at the end of its finalizer, it is still not accessible by any code. If the finalizer revived the object by making it accessible, the object will not be collected.
With regards to Dispose, the common Dispose pattern generally does all of the things that the finalizer would do, so there is no point in calling the finalizer; the GC.SuppressFinalize call that generally appears there is largely a performance optimization. Similarly, the invocation of Dispose(bool) from the finalizer is to avoid a memory leak in the case that an object was not properly disposed. Generally speaking, the difference between an explicit Dispose and a finalize is that the Dispose call will propagate the Disposal to any members which implement IDisposable, but the finalizer will not Dispose its members.
One question still remains -- when does all of this happen? Disposal is explicit, and I've mentioned that an object becomes a candidate for finalization when it is no longer accessible. The general assumption is that an object is no longer accessible when all references to it have fallen out of scope. It turns out that this is not entirely accurate. Take the following excerpt from the standard:
Note: Implementations might choose to analyze code to determine which references to an object can be used in the future. For instance, if a local variable that is in scope is the only existing reference to an object, but that local variable is never referred to in any possible continuation of execution from the current execution point in the procedure, an implementation might (but is not required to) treat the object as no longer in use.
What this basically says is that an object can be finalized as soon as it can't be used anymore, regardless of whether or not it is still in scope. Consider this code:
Foo bar = new Foo();
//other code that does not use bar
int i = 0;
Hopefully that will help clear up a lot of the confusion about what exactly happens to an object before and after death. Calling any of these steps a "destructor" is a flawed perspective and does not accurately reflect the underlying behaviors. (Slight inaccuracy: The specs do in fact refer to the finalizer as a "destructor". I don't like this choice of terminology.)