.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.
You 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:
What values should you use for the thresholds? These values may change between customer environments and for the same customer over time.
How will your components know what other applications on the same machine are doing with the same handles?
If you do trigger collections and the objects maintaining the handles are not garbage, you will end up paying for the collection but not benefit from it at all.
In the final analysis, using HandleCollector
is a crude optimization technique, and like most optimizations, you should avoid it. Rely instead on deterministic finalization.
There 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:
When you implement
Finalize()
, it’s important to call your base class’sFinalize()
method as well, to give the base class a chance to perform its cleanup:protected void Finalize() { /* Object cleanup here */ base.Finalize(); }
Note that the canonical .NET type
System.Object
has a do-nothing, protectedFinalize()
method so that you can always call it, regardless of whether your base classes actually provide their ownFinalize()
methods.Make sure to define
Finalize()
as a protected method. Avoid definingFinalize()
as a private method, because that precludes your subclasses from calling yourFinalize()
method. Interestingly enough, .NET uses reflection to invoke theFinalize()
method and isn’t affected by the visibility modifier.Avoid making blocking calls, because you’ll prevent finalization of all other objects in the queue until your blocking call returns.
Finalization must not rely on thread affinity to do the cleanup. Thread affinity is the assumption by a component designer that an instance of the component will always run on the same thread (although different objects can run on different threads).
Finalize()
will be called on a garbage-collection thread, not on any user thread. Thus, you will be unable to access any of your resources that are thread-specific, such as thread local storage or thread-relative static variables.Finalization must not rely on a specific order (e.g., Object A should release its resources only after Object B does). The two objects may be added to the finalization queue in any order.
It’s important to call the base-class implementation of
Finalize()
even in the face of exceptions. You do so by placing the call in atry/finally
statement:
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.
Get Programming .NET Components, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.