4.4. Object Finalization.NET objects are never told when they become garbage; they are simply overwritten when the managed heap is compacted. This presents you with a problem: if the object holds expensive resources (files, connections, communication ports, data structures, synchronization handles, and so on), how can it dispose of and release these resources? To address this problem, .NET provides object finalization. If the object has specific cleanup to do, it should implement a method called Finalize( ), defined as: protected void Finalize( ); When the garbage collector decides that an object is garbage, it checks the object metadata. If the object implements the Finalize( ) method, the garbage collector doesn't destroy the object. Instead, the garbage collector marks the object as reachable (so it will not be overwritten by heap compaction), then moves the object from its original graph to a special queue called the finalization queue. This queue is essentially just another object graph, and the root of the queue keeps the object reachable. The garbage collector then proceeds with collecting the garbage and compacting the heap. Meanwhile, a separate thread iterates over all the objects in the finalization queue, calling the Finalize( ) method on each and letting the objects do their cleanup. After calling Finalize( ), the garbage collector removes the object from the queue. 4.4.1. Explicit Garbage CollectionYou can trigger garbage collection explicitly with the static method Collect( ) of the GC class, defined in the System namespace: public static class GC { public static void Collect( ); /* Other methods and members */ } However, I recommend avoiding explicit garbage collection of any kind. Garbage collection is an expensive operation, which involves scanning of object graphs, thread context switches, thread suspension and resumption, potential disk access, and extensive use of reflection to read object metadata. The reason to initiate garbage collection is often because you want to have certain objects' Finalize( ) methods called to dispose of resources the objects hold. Instead of initiating garbage collection to achieve this, you can use deterministic finalization, which will be discussed later in this chapter.
You can also trigger garbage collection using the HandleCollector helper class, introduced in .NET 2.0 in the System.Runtime.InteropServices namespace: public sealed class HandleCollector { public HandleCollector(string name,int initialThreshold,int maximumThreshold); public HandleCollector(string namt initialThreshold); public void Add( ); public void Remove( ); public int InitialThreshold{get;} public int MaximumThreshold{get;} public int Count{get;} public string Name{get;} } HandleCollector allows you to keep track of allocations of expensive unmanaged resources, such as Windows or file handles. You use a HandleCollector object for each type of resource you manage. HandleCollector is meant to deal with objects that are not expensive in the amount of memory they consume, but that do hold onto expensive unmanaged handles. Whenever you allocate a new unmanaged resource monitored by HandleCollector, you call the Add( ) method. When you de-allocate such a resource in your Finalize( ) method, you call Remove( ). Internally, HandleCollector maintains a counter that it increments or decrements based on the calls to Add( ) or Remove( ). As such, HandleCollector functions as a simplistic reference counter for each handle type. When constructing a new HandleCollector object, you specify initial and maximum thresholds. As long as the number of resources allocated is under the initial threshold, there are no garbage-collection implications. If you call Add( ) and the resource counter exceeds the initial threshold (but is still under the maximum threshold), garbage collection may or may not take place, based on a self-tuning heuristic. If you call Add( ) and the resource counter exceeds the maximum threshold, garbage collection will always take place. Using HandleCollector raises several problematic issues:
In the final analysis, using HandleCollector is a crude optimization technique, and like most optimizations, you should avoid it. Rely instead on deterministic finalization. 4.4.2. Finalize( ) Method ImplementationThere is much more to object finalization than meets the eye. In particular, you should note that calling Finalize( ) is nondeterministic in time. This may postpone the release of resources the object holds and threaten the scalability and performance of the application. There are, however, ways to provide deterministic object finalization, addressed later in this chapter. To end this section, here are a number of points to be mindful of when implementing the Finalize( ) method:
protected virtual void Finalize( ) { try { /* Object cleanup here */ } finally { base.Finalize( ); } } Because these points are generic enough to apply to every class, the C# compiler has built-in support for generating template Finalize( ) code. In C#, you don't need to provide a Finalize( ) method; instead, you provide a C# destructor. The compiler converts the destructor definition to a Finalize( ) method, surrounding it in an exception-handling statement and calling your base class's Finalize( ) method automatically on your behalf. For example, for this C# class definition: public class MyClass { public MyClass( ) {} ~MyClass( ) { //Your destructor code goes here } } here's the code that is actually generated by the compiler: public class MyClass { public MyClass( ) {} protected virtual void Finalize( ) { try { //Your destructor code goes here } finally { base.Finalize( ); } } } If you try to define both a destructor and a Finalize( ) method, the compiler generates a compilation error. You will also get an error if you try to explicitly call your base class's Finalize( ) method. Finally, in the case of a class hierarchy, if all classes have destructors, the compiler-generated code calls every destructor, in order, from that of the lowest subclass to that of the topmost base class. |