Finalizers

Resources of an object are cleaned up in a Finalize method. Before an object is removed from memory, the Finalize method is called to allow for cleanup that is related to the object. Finalize methods are called finalizers. Every object inherits the Finalize method from the System.Object type. Here is the signature of the Finalize method:

  • protected virtual void Finalize()

Finalizers are not called programmatically; they are called automatically during the garbage collection cycle. Garbage collection is nondeterministic, which means that the finalizer may not be called immediately after the object no longer is needed. Unmanaged resources that might be released in a finalizer, such as files, database connections, and devices, might not be released in a timely manner. This delay in relinquishing resources can have adverse side effects on the current application and possibly other applications as well.

Instead of a Finalize method, C# has a destructor. Calling a Finalize method explicitly is not allowed and would cause a compiler error. Destructor methods are preceded with a tilde (~) and share the name of the class.

Here is the Destructor syntax:

~Classname()

The Destructor syntax has the following limitations:

  • Destructors do not have modifiers.

  • Destructors are protected and virtual by default.

  • Destructors have no parameters.

  • Destructors cannot be overloaded.

  • Destructors do not have a return type. The implied return type of a destructor is void.

The C# compiler converts destructors to Finalize methods, which override the inherited method. Here is a simple destructor:

~ZClass() {
    --propCount;
}

The C# compiler will emit a Finalize method for the ZClass. The Finalize method replaces the destructor in Microsoft Intermediate Language (MSIL) code. Figure 17-4 shows the ZClass class. Notice the Finalize method and the absence of the Destructor method.

A view of the ZClass class

Figure 17-4. A view of the ZClass class

The C# compiler generates the following MSIL code for the ZClass destructor (for clarity, extraneous code has been removed from the listing):

.method family hidebysig virtual instance void Finalize() cil managed
{
    .maxstack  2
    .try
    {
       IL_0001:  ldsfld      uint8 Donis.CSharpBook.ZClass::propCount
       IL_0006:  ldc.i4.1
       IL_0007:  sub
       IL_0008:  conv.u1
       IL_0009:  stsfld      uint8 Donis.CSharpBook.ZClass::propCount
       IL_000f:  leave.s     IL_0019
    }
    finally
    {
        IL_0011:  ldarg.0
        IL_0012:  call        instance void[mscorlib]System.Object::Finalize()
        IL_0018:  endfinally
    }
    IL_0019:  nop
    IL_001a:  ret
}

The Finalize method provided by the C# compiler contains a termination handler, which is the finally block. The try block contains the code from the original C# ZClass destructor. The termination handler calls the Finalize method of the base class. Because the call is in a termination handler, the finalizer of the base class is called even if an unhandled exception is raised in the destructor of the current class. This means that calls to the Finalize method propagate through the class hierarchy even when an exception is raised. This also means that developers should never attempt to call the destructor of the base class directly. The compiler-generated Finalize method assumes this responsibility.

Many developers have lamented the decision to associate finalizers with destructors. C++ developers are familiar with destructors. They are an integral part of the C++ language. This familiarity creates a level of expectation, which is honored only partially in C#. There are tangible differences between C++ and C# destructors, including the following:

  • C++ destructors can be called deterministically, whereas C# destructors are called nondeterministically.

  • C++ destructors can be virtual or nonvirtual, whereas C# destructors are implicitly virtual.

  • C++ destructors execute on the same thread that created the object, whereas C# destructors execute on a dedicated finalizer thread.

  • C++ destructors cannot be suppressed at run time, whereas C# destructors can be suppressed.

Note

For the remainder of this chapter, the terms destructor and finalizer are used synonymously.

As mentioned already, finalizers are called nondeterministically. In code, you cannot call them directly. When are finalizers called?

  • During the garbage collection process

  • When the AppDomain of an object is unloaded

  • When the GC.Collect method is called

  • When the CLR is shutting down

Knowing the specific reason for finalization can be helpful. You can confirm that finalization is initiated by an application unload or by a CLR shutdown. The AppDomain.IsFinalizingForUnload method, which is an instance method, returns true if the application domain is unloading and finalization has begun on objects in that domain. Otherwise, the method returns false. The Environment.HasShutdownStarted property is a static property that returns true if the CLR is shutting down or the application domain is unloading. Otherwise, the property returns false.

The Recycle application resurrects an instance of the ZClass type. When the object is being finalized, the Environment.HasShutdownStarted property is checked. If false, the reference to the object is re-established, which ends finalization. This resurrects the object. (We’ll discuss more about resurrection in the section titled "Resurrection" later in this chapter.) If the property is true, the object is not recycled. There is no reason to recycle an object if the current application domain is unloading or the CLR is shutting down. Here is the ZClass type from the Recycle application:

public class ZClass {
   public ZClass() {
       ++propCount;
       _count = propCount;
   }

   ~ZClass() {
       AppDomain current = AppDomain.CurrentDomain;
       if (!Environment.HasShutdownStarted) {
           Form1.obj = this;
           GC.ReRegisterForFinalize(this);
       }
   }

   private static int propCount = 0;
   private int _count;
   public int Count {
       get {
           return _count;
       }
   }
}

At application shutdown, finalizers have a limited amount of time to complete their tasks. This prevents an indefinite shutdown. Each Finalize method has 2 seconds to complete. If the time allotment is exceeded, that finalizer terminates, and the remaining finalizers then can execute. As a group, the Finalize methods have 40 seconds to complete all finalization chores. When this time limit is exceeded, the remaining finalizers are skipped. In terms of processing time, 40 seconds is nearly an eternity and should be sufficient to complete even the most elaborate housecleaning of an application.

In the following application, you can adjust the duration of the finalizer and view the results. Entering a number between 1 and 10 provides the best comparative results. Entering 1 from the command line causes the shortest destructor from a ZClass object. Larger numbers make the destructor run longer. Specify the value as a command-line argument:

using System;
using System.Threading;

namespace Donis.CSharpBook {
    public class Starter {
        private static void Main(string[] args) {
            Shutdown.ZTime = int.Parse(args[0]);
            ZClass[] obj = new ZClass[500];
            for(int count = 0; count < 500; ++count) {
                obj[count] = new ZClass();
            }
        }
    }

    public class Shutdown {
        static public int ZTime = 0;
    }

    public class ZClass {

        public ZClass() {
            ++globalcount;
            localcount = globalcount;
        }

        private int localcount = 0;
        private static int globalcount = 0;

        ~ZClass() {
            for(int i = 0; i < Shutdown.ZTime; ++i) {
                Thread.Sleep(50);
            }
            Console.WriteLine(localcount + " " +
                "ZClass destructor");
        }
    }
}

Finalizer Thread

The finalizer thread calls finalizers on objects waiting in the FReachable queue. After the finalizer is called, the object is removed from the FReachable queue and deleted from memory during the next garbage collection cycle. The finalizer thread executes the finalizer asynchronously. Finalizer threads service all finalizers of the application, which creates a potential bottleneck. For this reason, finalizers should be short and simple. A long finalizer can delay the finalization of the remaining objects on the FReachable queue. This extends the lifetime of those objects pending finalization and increases memory pressure on the managed heap. This in turn causes more garbage collection, which is costly.

Finalizer Considerations

When implementing finalizers, developers should consider several factors. Some of these considerations emphasize that finalizers should be avoided whenever possible. For example, you should never implement an empty finalizer. In the C++ language, an empty destructor is harmless. In C#, an empty destructor (finalizer) is costly, as explained in this section. These are the factors:

High expense of finalizers

As demonstrated several times already in this chapter, finalizers are expensive. At least two garbage collection cycles are required to collect a finalizable object that is not rooted. During the first garbage collection, the finalizable object is moved from the Finalizable queue to the FReachable queue. After the finalizer has been called, the memory of the object is reclaimed in a future garbage collection. The lifetime of objects referenced by the finalizable object are equally extended. They must wait for the finalizable object to be reclaimed before being released themselves. This extra object retention increases memory pressure, which causes additional garbage collection. The finalizable objects are promoted to later generations, which make a full garbage collection more likely, and full garbage collection is even more expensive.

The extra expense of defining a finalizable object actually starts at its allocation. When a finalizable object is created, a reference to the object must be added to the Finalizable queue. Objects without finalizers avoid this additional expense at allocation.

Lack of guarantee

Finalizers are not always called. Some of the reasons why a finalizer might not be called have already been mentioned. One such reason is the shutdown of the CLR by a host application. Another cause is if the finalizers exceed the remaining available time. In addition, you can programmatically suppress a finalizer with the GC.SuppressFinalize method. An asynchronous exception, such as a thread abort exception, can cause a finalizer not to run.

Multithreading

Finalizable objects are multithreaded. Object code and the finalizer execute on different threads. For this reason, certain activities should be avoided in the finalizer. Most notably, never access TLS associated with the object in the finalizer. Because the thread context has changed, using TLS in the finalizer is inappropriate.

The TLS sample application contains the following code, which uses TLS. This is a Windows Forms application. It contains a TMonitor class that logs messages to a file. The TMonitor class maintains a reference to several StreamWriter objects, which are stored in the TLS table of each thread. For this reason, threads accessing the same TMonitor object will use different StreamWriter objects that are retrieved from each thread’s individual TLS table. Therefore, each thread will write status messages to a different file. In the sample application, two threads are accessing a TMonitor instance. In the destructor of the TMonitor type, a StreamWriter is retrieved from TLS and closed. Which StreamWriter is closed? Is this the StreamWriter of the first or second thread? The answer is neither. The destructor is running on the finalizer thread, which is unrelated to the other threads. This thread has no StreamWriter reference, and an error is reported using the Console.WriteLine method. In Microsoft Visual Studio, Console.WriteLine in a Windows Forms application displays text in the Output window. The following application is for demonstration purposes only. In a finalizer, you should not reference other managed objects. A corrected version of this code is presented in the section titled "TLS Example" later in this chapter.

class TMonitor {
    public void WriteFile() {
        StreamWriter sw = Thread.GetData(Form1.Slot) as StreamWriter;
        if (sw == null) {
            sw = new StreamWriter(string.Format(
                @"C:{0}File.txt",
                Thread.CurrentThread.Name),
                true);
            Thread.SetData(Form1.Slot, sw);
        }
        sw.WriteLine(DateTime.Now.ToLongTimeString());
    }

    ~TMonitor() {
        StreamWriter sw = Thread.GetData(Form1.Slot) as StreamWriter;
        if (sw != null) {
            sw.Close();
        }
        else {
            Console.WriteLine("Error in destructor");
        }
    }
}

One finalizer thread

There is a single finalizer thread that services the FReachable queue. It calls pending finalizers of finalizable objects. The finalizer thread is different from other threads that might be accessing the object methods. Do not change the context of the finalization thread. This is not your thread. Changing the context of the finalization thread can have adverse effects on the finalization process.

Finalizers and virtual functions

Do not call virtual functions from finalizers. This can cause unexpected behavior such as inadvertent leaks and resources not being cleaned up. If overridden, there is no assurance that the derived class will provide the appropriate implementation related to the destructor. This is especially true for classes published in a class library, in which the developer of the derived class might have limited knowledge of the base class, including the destructor.

Look at the following sample code:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        private static void Main() {
            YClass obj = new YClass();
        }
    }

    public class ZClass {

        protected virtual void CloseResource() {
            Console.WriteLine("Closing ZClass resource.");
        }

        ~ZClass() {
            CloseResource();
        }
    }


    public class YClass : ZClass {

        protected override void CloseResource() {
            Console.WriteLine("Closing YClass resource.");
        }

        ~YClass() {
            CloseResource();
        }
    }
}

In the preceding code, the finalizer in the ZClass calls the CloseResource method, which is a virtual method. YClass is derived from ZClass and overrides the CloseResource method. An instance of the derived class is created in the Main method. At garbage collection, the following messages are displayed:

Closing YClass resource.
Closing YClass resource.

Yes, CloseResource of the derived class is called twice—from the ZClass and the YClass destructors. Because CloseResource is virtual in the base class, the ZClass destructor calls CloseResource on the derived type. Calling CloseResource incorrectly twice creates a couple of problems. First, an exception could be raised on the second attempt to close the resource. Second, the CloseResource method is not called in the ZClass. The ZClass resource is leaked and not removed.

Unpredictable order of finalization

Garbage collection on objects is not performed in a guaranteed sequence. For this reason, you do not want to access another managed object in the finalizer. There is no assurance that the other managed object has not been collected and removed from memory.

In the following code, the ZClass has a StreamWriter field. In the finalizer, the StreamWriter is closed. Normally, this is perfectly acceptable behavior, but not in a finalizer. In the application, an exception is raised in the finalizer during garbage collection. The StreamWriter object was collected before the finalizer of the ZClass is called. Therefore, an exception is raised in the finalizer:

public class ZClass {
    public ZClass() {
        sw = new StreamWriter("test.txt");
    }

    ~ZClass() {
        sw.Close();
    }

    private StreamWriter sw;
}

Resurrection

You can intentionally (or, more likely, inadvertently) resurrect an object permanently during the finalization process. At that time, the object is resurrected without the finalizer attached. Therefore, the resurrected object is not completely returned to live status and becomes a zombie. Zombies pose a distinct problem because of their dangling finalizers. Because a finalizer is not called on the resurrected object, proper cleanup is not performed when the zombie is collected later. The result could be a memory leak, a resource not being released, or other related problems. You can reconnect the finalizer manually with the GC.ReRegisterForFinalize method. Afterward, the object is normal and is no longer a zombie.

Conversely, another problem with resurrection is that the original finalizer might have executed. This could render the object unusable because needed resources might have been relinquished.

During finalization, a common cause of accidental resurrection is the creation of a new reference to the current object. At that moment, the object is resurrected. Objects with a reference cannot be collected. Despite being resurrected, the finalizer of the object runs to completion. In the following code, the current object is directly referenced in the finalizer. For most applications, indirect references are more likely the culprit. Developers are unlikely to place a direct reference to the current object in a destructor. Referencing another managed object in a finalizer can have a domino effect, which could cause an indirect reference and should be avoided. That object might access another managed object, which might reference another object and so on until something references the current object. Voilà—resurrection has occurred:

using System;
using System.IO;

namespace Donis.CSharpBook {

    public class Starter {

        private static void Main() {
            obj = new ZClass();
            obj.TimeStamp();
            obj = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            obj.TimeStamp(); // exception raised
        }

        public static ZClass obj;
    }

    public class ZClass {

        public ZClass() {
            sw = new StreamWriter("test.txt");
        }

        public void TimeStamp() {
            sw.WriteLine(DateTime.Now.ToLongTimeString());
        }

        ~ZClass() {
            Starter.obj = this;
            sw.Close();
            sw = null;
        }
        static private StreamWriter sw;
    }
}

In the preceding code, the first statement in the ZClass destructor resurrects the object. That statement assigns the object reference to a static field of the Starter class. Because this creates another reference, the object will be resurrected. Next in the destructor, the underlying file of the StreamWriter instance is then closed. In Main, where the program starts, an instance of the ZClass is created. The instance is assigned to a static field of the Starter class. Afterward, the reference is assigned null, and garbage collection is forced with GC.Collect. This kills and then resurrects the object. The next call on the object (obj.TimeStamp) raises an exception because the underlying file has been closed in the previous finalization. The TimeStamp method refers to the file, which is now closed.

The following destructor is a modification of the previous code that reconnects the finalizer upon resurrection. The first statement confirms whether the CLR is shutting down. If true, the managed application is exiting. In that circumstance, it would be nonsensical to resurrect the object. In the if block, the object is resurrected and the finalizer is reattached:

~ZClass() {
    if (Environment.HasShutdownStarted == false) {
        Starter.obj = this;
        sw.Close();
        sw = null;
        GC.ReRegisterForFinalize(this);
    }
}

The TimeStamp method must also be updated. The StreamWriter instance might be null. This is checked in the revised TimeStamp method. If null, the sw reference is reinitialized. The file resource is then available again for writing:

public void TimeStamp() {
    if (sw == null) {
        sw = new StreamWriter("test.txt", true);
    }
    sw.WriteLine(DateTime.Now.ToLongTimeString());
    sw.Flush();
}

Here is the revised application:

using System;
using System.IO;

namespace Donis.CSharpBook {

    public class Starter {
        private static void Main() {
            obj = new ZClass();
            obj.TimeStamp();
            obj = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            obj.TimeStamp();  // exception not raised
        }

        public static ZClass obj;
    }

    public class ZClass {

        public ZClass() {
            sw = new StreamWriter("test.txt", true);
        }

        public void TimeStamp() {
            if (sw == null) {
                sw = new StreamWriter("test.txt", true);
            }
            sw.WriteLine(DateTime.Now.ToLongTimeString());
            sw.Flush();
        }

        ~ZClass() {
            if (Environment.HasShutdownStarted == false) {
                Starter.obj = this;
                sw.Close();
                sw = null;
                GC.ReRegisterForFinalize(this);
            }
        }

        static private StreamWriter sw;
    }
}

Finalizers and reentrancy

A finalizer is reentrant. The best example of this is complete resurrection, in which the finalizer is reattached. In that circumstance, the finalizer is called at least twice. When a finalizer is called more than once, resurrection is the likely culprit. Redundant calls on a finalizer should not cause a logic error or an exception.

Deep object graphs

A deep object graph can make garbage collection more expensive. Roots and branches of the object graph can be anchored with a finalizable object. As mentioned, the lifetime of a finalizable object is extended to encompass at least two garbage collections. All objects on the finalizable object branch, even nonfinalizable objects, have their lives similarly extended. Therefore, one finalizable object can keep several other objects from being garbage-collected. Deeper object graphs by definition have longer branches and can extend the problem to more objects.

Finalizer race conditions

The finalizer can execute at the same time as other functions of the same object. Because the finalizer executes on a dedicated thread, other functions of the finalizable object could be running when finalization starts. This can cause a race condition to occur between that behavior and finalization. Standard thread-synchronization techniques can protect against a finalization race condition.

In the following code, MethodA is called on a ZClass instance. MethodA has a built-in delay. Shortly thereafter, the related object is set to null and collected. Because MethodA is stalled, the finalizer then runs concurrently with MethodA. The race has begun! The results are unpredictable:

public class Starter {

    private static void Main() {
        Thread t = new Thread(
            new ThreadStart(MethodA));
        obj = new ZClass();
        t.Start();
        obj = null;
        GC.Collect();
    }

    private static void MethodA() {
        obj.MethodB();
    }

    private static ZClass obj;
}

public class ZClass {

    public void MethodB() {
        Console.WriteLine("ZClass.MethodB Started");
        Thread.Sleep(1500);
        Console.WriteLine("ZClass.MethodB Finished");
    }

    ~ZClass() {
        Console.WriteLine("ZClass.~ZClass Started");
        // Destructor operation
        Console.WriteLine("ZClass.~ZClass Finished");
    }
}

Constructors

An exception in a constructor does not prevent an object from being finalized. The finalizer is called regardless of the success of the constructor, which can create an interesting dilemma in which an object that is unsuccessfully constructed is still finalized. Cleanup of a partially constructed object can pose risks.

The following code demonstrates the problems that can occur when an exception is raised in a constructor. The exception is caught by the exception handler in Main. At that time, the object exists but is not correctly initialized. Despite this, the finalizer is called as the application exits and attempts to clean up fully for the object. In actual code (not stubbed, as it is in the following sample), this could cause an exception or other fault:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        private static void Main() {
            try {
                ZClass obj = new ZClass();
            }
            catch {
            }
        }
    }

    public class ZClass {

        public ZClass() {
            Console.WriteLine("ZClass constructor");
            throw new Exception("Error");
        }

        ~ZClass() {
            Console.WriteLine("ZClass destructor");
        }
    }
}

The following code resolves the problem of a finalizable object with a failed constructor. If the constructor does not complete successfully, GC.SuppressFinalize prevents the finalizer from being called later. In addition, a flag is maintained that indicates the state of the object. The bPartial flag is set to true if the constructor fails. The flag is checked in instance methods. If the flag is true, the methods raise an exception because the object might not be stable:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            try {
                ZClass obj = null;
                obj = new ZClass();
                if (obj != null) {
                    obj.MethodA();
                }
            }
            catch(Exception ex) {
                Console.WriteLine(ex.Message);
            }
        }
    }

    public class ZClass {

        public ZClass() {
            try {
                Console.WriteLine("ZClass constructor");
                throw new Exception("Error");
            }
            catch {
                GC.SuppressFinalize(this);
                bPartial = true;
            }
        }

        public void MethodA() {
            if (bPartial) {
                throw new Exception("Partial construction error");
            }
            Console.WriteLine("ZClass.MethodA");
        }

        ~ZClass() {
            Console.WriteLine("ZClass destructor");
        }

        private bool bPartial = false;
    }
}

The Console class and finalization

You can safely use the Console class in a finalizer. It is exempted from the rule not to use other managed classes in a finalizer. The Console class is specially written for use during finalization.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.144.228