Section 4.4. Object Finalization


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 Collection

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.

Increasing Memory Pressure

Normally, garbage collection is triggered when the managed heap is exhaustedthe garbage collector watches the heap, and when the memory usage exceeds a certain threshold, it triggers garbage collection. The GC class provides the AddMemoryPressure( ) method for lowering that threshold, causing more frequent collections:

     public static class GC     {        public static void AddMemoryPressure(long pressure);        public static void RemoveMemoryPressure(long pressure);        /* Other methods and members */     } 

You can also remove the added pressure via the RemoveMemoryPressure( ) method, but you can only remove pressure you have explicitly addedyou can't lower the threshold below its default setting.

There are two possible uses for adding memory pressure. The first is when dealing with objects that are resource-intensive but memory-cheap. Increasing the memory pressure will result in more collections and will potentially collect the expensive objects, whose Finalize( ) methods will be called to release those resources. The problem is that it is difficult to tell by how much to increase the pressure, and doing so will not always yield deterministic results. I recommend that you avoid adding memory pressure in such cases, and instead use deterministic finalization (discussed later).

The second use for adding memory pressure is during stress testing. If you want to test how your application functions in an environment with intense garbage collection, AddMemoryPressure( ) offers a simple and easy way to find out. Such use of AddMemoryPressure( ) is benign and acceptable.


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.

4.4.2. Finalize( ) Method Implementation

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's Finalize( ) 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, protected Finalize( ) method so that you can always call it, regardless of whether your base classes actually provide their own Finalize( ) methods.

  • Make sure to define Finalize( ) as a protected method. Avoid defining Finalize( ) as a private method, because that precludes your subclasses from calling your Finalize( ) method. Interestingly enough, .NET uses reflection to invoke the Finalize( ) 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 a try/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.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net