Finalization is one of the most complicated and obscure areas of .NET. Most developers don’t actually bother writing finalizers, but if you’re designing and implementing any framework of considerable size, you are probably going to stumble across a well-defined set of issues. And yes, it’s not that simple to get it right – performance issues, concurrency issues, throttling issues, GC issues – there is a plethora of rocks to wreck your boat on.
There are many resources out there that assist in getting finalization right, so I won’t be repeating what others have written. However, I would like to address an area that is just as important – the question of whether you can count on your finalizer. To be more specific, whether you can count on your finalizer getting executed.
Finalization is a mechanism for releasing unmanaged resources. As such, it is often quite important that the finalizer execute – or else a resource leak of some sort might arise. Some of these leaks are restricted to the boundaries of a single process, and will go away once the process terminates. Other leaks are not restricted to a single process or even a single machine, and have the potential of virally bringing an entire system down.
For example, consider a resource such as a file handle. A file handle is an OS resource that is restricted to a single process, so if you forget to close a file handle, it will be closed for you automatically when the process terminates. On the other hand, the file that you created does not go away automatically with your process (with the exception of files explicitly created for temporary storage with FILE_ATTRIBUTE_TEMPORARY) – it is a resource on your file system that outlives a single process, and if you have a leak of temporary files getting created without being deleted, you might run out of disk space, which is a system-wide problem.
So once we have established that finalizers are important and that their execution could be critical for the proper operation of a system, why would a finalizer not get called? What can prevent your carefully crafted resource disposal mechanism from executing?
Well, there are at least four scenarios in which the finalizer won’t get called. Some of them might be regarded as corner cases, other might be more common. However, the primary thing to keep in mind is that if the resource you’re protecting has the potential of bringing the system down, then you must consider a form of protection outside finalization to proactively defend yourself against the case when the finalizer won’t get called. An example of a proactive defensive approach would be what .NET remoting uses to implement distributed garbage collection. When a remote object is instantiated by a client, the server infrastructure could keep that object alive as long as the client didn’t send an explicit “destroy object” request. However, if the client forgets to destroy the object, or the message gets lost, or the network connection fails, the server object would then remain leaked forever. And that’s exactly why .NET remoting implements a lifetime management mechanism with leases and sponsors, making everything significantly more complicated but addressing the potential leak.
So without further ado, why would a finalizer not get called?
Well, the simplest way of all to prevent finalizers from executing is get the finalizer thread to do something else. Indefinitely. There is just one finalizer thread, so if you nailed it down, no one else is there to run finalizers on your behalf. This is significantly less complicated than it sounds, because all you need to do is put a Thread.Sleep(Timeout.Infinite) in your rogue object’s finalizer, and no other finalizer will ever get called. This scenario has been extensively covered by Tess and myself, and can be diagnosed quite easily using SOS. (Obviously there are also more realistic and less malicious ways to get the same effect, such as waiting for some resource that is never available, waiting for a message from the network that never arrives, etc.)
Another way to prevent a finalizer from executing is rudely aborting the application. This can be requested by your CLR host, another process, or even a rogue piece of code inside your process. For example, the Environment.FailFast method rudely aborts the process, without allowing finalizers to run. Alternatively, the TerminateProcess function rudely terminates the process without any cleanup mechanisms. Not only finalizers, but even finally blocks won’t run in these cases.
A third scenario where a finalizer won’t run is a corner case condition where the application experiences an OutOfMemoryException. Consider what happens – if the application is supposed to terminate because of an OutOfMemoryException, surely I would like my finalizer to run. However, it might as well be the case that this is the first time ever my finalizer wants to run in that execution. So if it’s the first time (and I haven’t used NGEN), then the finalizer has to be JIT-compiled first before it can be executed. But to JIT-compile it, memory must be allocated. But hey! – we’re in an out of memory situation. Bummer. No finalizer for you – come back one year. This specific scenario can be alleviated by deriving your finalizeable resource class from CriticalFinalizerObject. But it kind of starts showing you that ensuring that some code executes in every possible scenario is significantly harder than it seems.
A final scenario (there might be more but that’s all I could think of right now) in which a finalizer won’t run is a specific case of process termination or AppDomain unload. When the process terminates or the AppDomain unloads, the CLR attempts to run all finalizers for objects that requested finalization. However, in this specific scenario, it imposes a limit on the time allotted to these finalizers. Each individual finalizer gets up to 2 seconds to run, and all finalizers combined get up to 40 seconds. So no matter what you do, if your finalization work takes more than 2 seconds, or if the combined finalization work of all objects takes more than 40 seconds, some finalizers are not guaranteed to run, and some others might be interrupted in the middle. You can program defensively against this situation using Environment.HasShutdownStarted and AppDomain.IsFinalizingForUnload – but this is another demonstration of how non-trivial it is to ensure that something happens in every possible scenario.
Summing things up, finalization is not a bad feature. It is a must in the world of unmanaged resources which we live in. However, there are significant pitfalls associated with finalization that we must be aware of – and one of them is that we can’t rely on finalization one hundred percent. So if after reading this post you no longer take it for granted that finalizers will take care of all the troubles of the world for you, you are already in a better position than you were in the beginning.