4.6. Object Life Cycle Management

 < Day Day Up > 

Memory allocation and deallocation have always been the bane of developers. Even the most experienced C++ and COM programmer is faced with memory leaks and attempts to access nonexistent objects that either never existed or have already been destroyed. In an effort to remove these responsibilities from the programmer, .NET implements a memory management system that features a managed heap and automatic Garbage Collection.

Recall from Chapter 2, "C# Language Fundamentals," that the managed heap is a pre-allocated area of memory that .NET uses to store reference types and data. Each time an instance of a class is created, it receives memory from the heap. This is a faster and cleaner solution than programming environments that rely on the operating system to handle memory allocation.

Allocating memory from the stack is straightforward: A pointer keeps track of the next free memory address and allocates memory from the top of the heap. The important thing to note about the allocated memory is that it is always contiguous. There is no fragmentation or complex overhead to keep track of free memory blocks. Of course, at some point the heap is exhausted and unused space must be recovered. This is where the .NET automatic Garbage Collection comes into play.

.NET Garbage Collection

Each time a managed object is created, .NET keeps track of it in a tree-like graph of nodes that associates each object with the object that created or uses it. In addition, each time another client references an object or a reference is assigned to a variable, the graph is updated. At the top of this graph is a list of roots, or parts of the application that exist as long at the program is running (see Figure 4-9). These include static variables, CPU registers, and any local or parameter variables that refer to objects on the managed heap. These serve as the starting point from which the .NET Framework uses a reference-tracing technique to remove objects from the heap and reclaim memory.

Figure 4-9. .NET Garbage Collection process


The Garbage Collection process begins when some memory threshold is reached. At this point, the Garbage Collector (GC) searches through the graph of objects and marks those that are "reachable." These are kept alive while the unreachable ones are considered to be garbage. The next step is to remove the unreferenced objects (garbage) and compact the heap memory. This is a complicated process because the collector must deal with the twin tasks of updating all old references to the new object addresses and ensuring that the state of the heap is not altered as Garbage Collection takes place.

The details of Garbage Collection are not as important to the programmer as the fact that it is a nondeterministic (occurs unpredictably) event that deals with managed resources only. This leaves the programmer facing two problems: how to dispose of unmanaged resources such as files or network connections, and how to dispose of them in a timely manner. The solution to the first problem is to implement a method named Finalize that manages object cleanup; the second is solved by adding a Dispose method that can be called to release resources before Garbage Collection occurs. As we will see, these two methods do not operate autonomously. Proper object termination requires a solution that coordinates the actions of both methods.

Core Note

Garbage Collection typically occurs when the CLR detects that some memory threshold has been reached. However, there is a static method GC.Collect that can be called to trigger Garbage Collection. It can be useful under controlled conditions while debugging and testing, but should not be used as part of an application.


Object Finalization

Objects that contain a Finalize method are treated differently during both object creation and Garbage Collection than those that do not contain a Finalize method. When an object implementing a Finalize method is created, space is allocated on the heap in the usual manner. In addition, a pointer to the object is placed in the finalization queue (see Figure 4-9). During Garbage Collection, the GC scans the finalization queue searching for pointers to objects that are no longer reachable. Those found are moved to the freachable queue. The objects referenced in this queue remain alive, so that a special background thread can scan the freachable queue and execute the Finalize method on each referenced object. The memory for these objects is not released until Garbage Collection occurs again.

To implement Finalize correctly, you should be aware of several issues:

  • Finalization degrades performance due to the increased overhead. Only use it when the object holds resources not managed by the CLR.

  • Objects may be placed in the freachable queue in any order. Therefore, your Finalize code should not reference other objects that use finalization, because they may have already been processed.

  • Call the base Finalize method within your Finalize method so it can perform any cleanup: base.Finalize().

  • Finalization code that fails to complete execution prevents the background thread from executing the Finalize method of any other objects in the queue. Infinite loops or synchronization locks with infinite timeouts are always to be avoided, but are particularly deleterious when part of the cleanup code.

It turns out that you do not have to implement Finalize directly. Instead, you can create a destructor and place the finalization code in it. The compiler converts the destructor code into a Finalize method that provides exception handling, includes a call to the base class Finalize, and contains the code entered into the destructor:

 Public class Chair {    public Chair() { }    ~Chair()   // Destructor    {  // finalization code  } } 

Note that an attempt to code both a destructor and Finalize method results in a compiler error.

As it stands, this finalization approach suffers from its dependency on the GC to implement the Finalize method whenever it chooses. Performance and scalability are adversely affected when expensive resources cannot be released when they are no longer needed. Fortunately, the CLR provides a way to notify an object to perform cleanup operations and make itself unavailable. This deterministic finalization relies on a public Dispose method that a client is responsible for calling.

IDisposable.Dispose()

Although the Dispose method can be implemented independently, the recommended convention is to use it as a member of the IDisposable interface. This allows a client to take advantage of the fact that an object can be tested for the existence of an interface. Only if it detects IDisposable does it attempt to call the Dispose method. Listing 4-10 presents a general pattern for calling the Dispose method.

Listing 4-10. Pattern for Calling Dispose()
 public class MyConnections: IDisposable {    public void Dispose()    {       // code to dispose of resources       base.Dispose(); // include call to base Dispose()    }    public void UseResources() { } } // Client code to call Dispose() class MyApp {    public static void Main()    {       MyConnections connObj;       connObj = new MyConnections();       try       {          connObj.UseResources();       }       finally   // Call dispose() if it exists       {          IDisposable testDisp;          testDisp = connObj as IDisposable;          if(testDisp != null)             { testDisp.Dispose(); }       }    } 

This code takes advantage of the finally block to ensure that Dispose is called even if an exception occurs. Note that you can shorten this code by replacing the try/finally block with a using construct that generates the equivalent code:

 Using(connObj) { connObj.UseResources() } 

Using Dispose and Finalize

When Dispose is executed, the object's unmanaged resources are released and the object is effectively disposed of. This raises a couple of questions: First, what happens if Dispose is called after the resources are released? And second, if Finalize is implemented, how do we prevent the GC from executing it since cleanup has already occurred?

The easiest way to handle calls to a disposed object's Dispose method is to raise an exception. In fact, the ObjectDisposedException exception is available for this purpose. To implement this, add a boolean property that is set to true when Dispose is first called. On subsequent calls, the object checks this value and throws an exception if it is true.

Because there is no guarantee that a client will call Dispose, Finalize should also be implemented when resource cleanup is required. Typically, the same cleanup method is used by both, so there is no need for the GC to perform finalization if Dispose has already been called. The solution is to execute the SuppressFinalize method when Dispose is called. This static method, which takes an object as a parameter, notifies the GC not to place the object on the freachable queue.

Listing 4-11 shows how these ideas are incorporated in actual code.

Listing 4-11. Pattern for Implementing Dispose() and Finalize()
 public class MyConnections: IDisposable {    private bool isDisposed = false;    protected bool Disposed    {       get{ return isDisposed;}    }    public void Dispose()    {       if (isDisposed == false)       {          CleanUp();          IsDisposed = true;          GC.SuppressFinalize(this);        }    }    protected virtual void CleanUp()    {       // cleanup code here    }    ~MyConnections()   // Destructor that creates Finalize()    { CleanUp(); }    public void UseResources()    {       // code to perform actions       if(Disposed)       {          throw new ObjectDisposedException                ("Object has been disposed of");       }    } } // Inheriting class that implements its own cleanup public class DBConnections: MyConnections {    protected override void CleanUp()    {       // implement cleanup here       base.CleanUp();    } } 

The key features of this code include the following:

  • A common method, CleanUp, has been introduced and is called from both Dispose and Finalize . It is defined as protected and virtual, and contains no concrete code.

  • Classes that inherit from the base class MyConnections are responsible for implementing the CleanUp. As part of this, they must be sure to call the Cleanup method of the base class. This ensures that cleanup code runs on all levels of the class hierarchy.

  • The read-only property Disposed has been added and is checked before methods in the base class are executed.

In summary, the .NET Garbage Collection scheme is designed to allow programmers to focus on their application logic and not deal with details of memory allocation and deallocation. It works well as long as the objects are dealing with managed resources. However, when there are valuable unmanaged resources at stake, a deterministic method of freeing them is required. This section has shown how the Dispose and Finalize methods can be used in concert to manage this aspect of an object's life cycle.

     < Day Day Up > 


    Core C# and  .NET
    Core C# and .NET
    ISBN: 131472275
    EAN: N/A
    Year: 2005
    Pages: 219

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