Garbage collection is a key responsibility of the runtime. Nevertheless, it is important to recognize that the garbage collection process centers on the code’s memory utilization. It is not about the cleaning up of file handles, database connection strings, ports, or other limited resources.
Finalizers allow developers to write code that will clean up a class’s resources. Unlike constructors that are called explicitly using the new operator, finalizers cannot be called explicitly from within the code. There is no new equivalent such as a delete operator. Rather, the garbage collector is responsible for calling a finalizer on an object instance. Therefore, developers cannot determine at compile time exactly when the finalizer will execute. All they know is that the finalizer will run sometime between when an object was last used and generally when the application shuts down normally. The deliberate injection of incertitude with the word “Generally” highlights the fact that finalizers might not execute. This possibility is obvious when you consider that a process might terminate abnormally. For instance, events such as the computer being turned off or a forced termination of the process, such as when debugging the process, will prevent the finalizer from running. However, with .NET Core, even under normal circumstances, finalizers may not get processed before the application shuts down. As we shall see in the next section, it thus may be necessary to take additional action to register finalization activities with other mechanisms.
The finalizer declaration is identical to the destructor syntax of C#’s predecessor—that is, C++. As shown in Listing 10.15, the finalizer declaration is prefixed with a tilde before the name of the class.
Finalizers do not allow any parameters to be passed, so they cannot be overloaded. Furthermore, finalizers cannot be called explicitly—that is, only the garbage collector can invoke a finalizer. Access modifiers on finalizers are therefore meaningless, and as such, they are not supported. Finalizers in base classes will be invoked automatically as part of an object finalization call.
Because the garbage collector handles all memory management, finalizers are not responsible for de-allocating memory. Rather, they are responsible for freeing up resources such as database connections and file handles—resources that require an explicit activity that the garbage collector doesn’t know about.
In the finalizer shown in Listing 10.15, we start by disposing of the FileStream. This step is optional because the FileStream has its own finalizer that provides the same functionality as Dispose(). The purpose of invoking Dispose() now is to ensure that it is cleaned up when TemporaryFileStream is finalized, since the latter is responsible for instantiating the former. Without the explicit invocation of Stream?.Dispose(), the garbage collector will clean it up independently from the TemporaryFileStream once the TemporaryFileStream object is garbage collected and releases its reference on the FileStream object. That said, if we didn’t need a finalizer for resource cleanup anyway, it would not make sense to define a finalizer just for invoking FileStream.Dispose(). In fact, limiting the need for a finalizer to only objects that need resource cleanup that the runtime isn’t already aware of (resources that don’t have finalizers) is an important guideline that significantly reduces the number of scenarios where it is necessary to implement a finalizer.
In Listing 10.15, the purpose of the finalizer is to delete the file9—an unmanaged resource in this case. Hence we have the call to File?.Delete(). Now, when the finalizers are executed, the file will get cleaned up.
Finalizers execute on an unspecified thread, making their execution even less deterministic. This indeterminate nature makes an unhandled exception within a finalizer (outside of the debugger) likely to crash the application—and the source of this problem is difficult to diagnose because the circumstances that led to the exception are not clear. From the user’s perspective, the unhandled exception will be thrown relatively randomly and with little regard for any action the user was performing. For this reason, you should take care to avoid exceptions within finalizers. Instead, you should use defensive programming techniques such as checking for null (refer to the use of the null-conditional operator in Listing 10.15). In fact, it is advisable to catch all exceptions in the finalizer and report them via an alternative means (such as logging or via the user interface) rather than keeping them as unhandled exceptions. This guideline leads to the try/catch block surrounding the Delete() invocation.
Another potential option to force finalizers to execute is to invoke System.GC.WaitForPendingFinalizers(). When this method is invoked, the current thread will be suspended until all finalizers for objects that are no longer referenced have executed.
The problem with finalizers on their own is that they don’t support deterministic finalization (the ability to know when a finalizer will run). Rather, finalizers serve the important role of being a backup mechanism for cleaning up resources if a developer using a class neglects to call the requisite cleanup code explicitly.
For example, consider the TemporaryFileStream, which includes not only a finalizer but also a Close() method. This class uses a file resource that could potentially consume a significant amount of disk space. The developer using TemporaryFileStream can explicitly call Close() to restore the disk space.
Providing a method for deterministic finalization is important because it eliminates a dependency on the indeterminate timing behavior of the finalizer. Even if the developer fails to call Close() explicitly, the finalizer will take care of the call. In such a case, the finalizer will run later than if it was called explicitly.
Because of the importance of deterministic finalization, the base class library includes a specific interface for the pattern and C# integrates the pattern into the language. The IDisposable interface defines the details of the pattern with a single method called Dispose(), which developers call on a resource class to “dispose” of the consumed resources. Listing 10.16 demonstrates the IDisposable interface and some code for calling it.
From Program.Search(), there is an explicit call to Dispose() after using the TemporaryFileStream. Dispose() is the method responsible for cleaning up the resources (in this case, a file) that are not related to memory and therefore are subject to cleanup implicitly by the garbage collector. Nevertheless, the execution here contains a hole that would prevent execution of Dispose()—namely, the chance that an exception will occur between the time when TemporaryFileStream is instantiated and the time when Dispose() is called. If this happens, Dispose() will not be invoked and the resource cleanup will have to rely on the finalizer. To avoid this problem, callers need to implement a try/finally block. Instead of requiring programmers to code such a block explicitly, C# provides a using statement expressly for the purpose (see Listing 10.17).
In the first highlighted code snippet, the resultant CIL code is identical to the code that would be created if the programmer specified an explicit try/finally block, where fileStream.Dispose() is called in the finally block. The using statement, however, provides a syntax shortcut for the try/finally block.
Within this using statement, you can instantiate more than one variable by separating each variable from the others with a comma. The key considerations are that all variables must be of the same type, the type must implement IDisposable, and initialization occurs at the time of declaration. To enforce the use of the same type, the data type is specified only once rather than before each variable declaration.
C# 8.0 introduces a potential simplification with regard to resource cleanup. As shown in the second highlighted snippet of Listing 10.17, you can prefix the declaration of a disposable resource (one that implements IDisposable) with the using keyword. As is the case with the using statement, this will generate the try/finally behavior, with the finally block placed just before the variable goes out of scope (in this case, before the closing curly brace of the Search() method). One additional constraint on the using declaration is that the variable is read-only, so it can’t be assigned a different value.
There are several additional noteworthy items to point out in Listing 10.16. First, the IDisposable.Dispose() method contains an important call to System.GC.SuppressFinalize(). Its purpose is to remove the TemporaryFileStream class instance from the finalization (f-reachable) queue. This is possible because all cleanup was done in the Dispose() method rather than waiting for the finalizer to execute.
Without the call to SuppressFinalize(), the instance of the object will be included in the f-reachable queue—a list of all the objects that are mostly ready for garbage collection, except they also have finalization implementations. The runtime cannot garbage-collect objects with finalizers until after their finalization methods have been called. However, garbage collection itself does not call the finalization method. Rather, references to finalization objects are added to the f-reachable queue and are processed by an additional thread at a time deemed appropriate based on the execution context. In an ironic twist, this approach delays garbage collection for the managed resources—when it is most likely that these very resources should be cleaned up earlier. The reason for the delay is that the f-reachable queue is a list of “references”; as such, the objects are not considered garbage until after their finalization methods are called and the object references are removed from the f-reachable queue.
For this reason, Dispose() invokes System.GC.SuppressFinalize. Invoking this method informs the runtime that it should not add this object to the finalization queue, but instead should allow the garbage collector to de-allocate the object when it no longer has any references (including any f-reachable references).
Second, Dispose() calls Dispose(bool disposing) with an argument of true. The result is that the Dispose() method on Stream is invoked (cleaning up its resources and suppressing its finalization). Next, the temporary file itself is deleted immediately upon calling Dispose(). This important call eliminates the need to wait for the finalization queue to be processed before cleaning up potentially expensive resources.
Third, rather than calling Close(), the finalizer now calls Dispose(bool disposing) with an argument of false. The result is that Stream is not closed (disposed) even though the file is deleted. The condition around closing Stream ensures that if Dispose(bool disposing) is called from the finalizer, the Stream instance itself will also be queued up for finalization processing (or possibly it would have already run depending on the order). Therefore, when executing the finalizer, objects owned by the managed resource should not be cleaned up, as this action will be the responsibility of the finalization queue.
Fourth, you should use caution when creating both a Close() type and a Dispose() method. It is not clear by looking at only the API that Close() calls Dispose(), so developers will be left wondering whether they need to explicitly call Close() and Dispose().
Fifth, to increase the probability that the functionality defined in the finalizer will execute before a process shuts down even in .NET Core, you should register the code with the AppDomain.CurrentDomain.ProcessExit event handler. Any finalization code registered with this event handler will be invoked, barring an abnormal process termination (discussed in the next section).
Although finalizers are similar to destructors in C++, the fact that their execution cannot be determined at compile time makes them distinctly different. The garbage collector calls C# finalizers sometime after they were last used but before the program shuts down; C++ destructors are automatically called when the object (not a pointer) goes out of scope.
Although running the garbage collector can be a relatively expensive process, the fact that garbage collection is intelligent enough to delay running until process utilization is somewhat reduced offers an advantage over deterministic destructors, which will run at compile-time–defined locations, even when a processor is in high demand.
Starting with .NET Core, the finalizers may not execute when a process is shutting down. To increase the likelihood of execution, the finalization activities should be registered to execute when the process10 shuts down. For this reason, notice the statement involving ProcessExit in the SampleUnmanagedResource constructor in Listing 10.18, with Output 10.3 showing the result when no arguments are passed to the program. (The code involves LINQ, event registration, and special attributes,11 which aren’t covered until Chapters 12, 14, and 18, respectively.)
Ignoring the WriteLine() statements throughout, the code begins with an invocation of DoStuff(), which instantiates the SampleUnmanagedResource.
When instantiating SampleUnmanagedResource, we simulate the instantiation of both managed and unmanaged resources using simple WriteLine() invocations. Next, we declare a delegate—called a handler—that will execute when the process exits. The handler leverages a WeakReference to the SampleUnmanagedResource instance and invokes Dispose() on the instance if there is still an instance to invoke. The WeakReference is necessary to ensure that ProcessExit doesn’t maintain a reference to the instance, thereby preventing the garbage collector from cleaning up the unmanaged resource after it goes out of scope and finalization has run. The handler is registered with AppDomain.CurrentDomain.ProcessExit and saved into the ProcessExitHandler property. The latter step is necessary so that we can remove the handler from the AppDomain.CurrentDomain.ProcessExit event when Dispose() executes, rather than have Dispose() execute repeatedly and unnecessarily.
Back in the DoStuff() method, we check whether the command-line argument -Dispose is specified as an argument when starting the program. If it is, then Dispose() would be invoked and neither the finalizer nor the ProcessExit handler will get invoked. Upon the exit from DoStuff(), the instance of SampleUnmanagedResource no longer has a root reference. However, when the garbage collector runs, it sees the finalizer and adds the resource to the finalization queue.
When the process begins to shut down, and assuming neither Dispose() nor the finalizer has executed on the SampleUnmanagedResource instance as yet, the AppDomain.CurrentDomain.ProcessExit event fires and invokes the handler, which in turn invokes Dispose(). The key difference in the Dispose() method from Listing 10.16 is that we unregister from AppDomain.CurrentDomain.ProcessExit so that Dispose() doesn’t get invoked again during the process exit if it was invoked earlier.
Note that while you can invoke GC.Collect() followed by GC.WaitForPendingFinalizers() to force all finalizers for objects without root references to run, and you could theoretically even do this right before the process exits, doing so is fallible. The first caveat is that library projects can’t invoke these methods immediately before the process exits because there is no instance of Main(). The second caveat is that even simple things like static references will be root references, so that static object instances will not be cleaned up. For this reason, using the ProcessExit handler is the preferred approach.
Even when an exception propagates out of a constructor, the object is still instantiated, although no new instance is returned by the new operator. If the type defines a finalizer, the method will run when the object becomes eligible for garbage collection (providing additional motivation to ensure the finalize method can run on partially constructed objects). Also note that if a constructor prematurely shares its this reference, it will still be accessible even if the constructor throws an exception. Do not allow this scenario to occur.
By the time an object’s finalization method is called, all references to the object have disappeared and the only step before garbage collection is running the finalization code. Even so, it is possible to add a reference inadvertently for a finalization object back into the root reference’s graph. In such a case, the re-referenced object will no longer be inaccessible; in turn, it will not be ready for garbage collection. However, if the finalization method for the object has already run, it will not run again unless it is explicitly marked for finalization (using the GC.ReRegisterFinalize() method).
Obviously, resurrecting objects in this manner is peculiar behavior, and you should generally avoid it. Finalization code should be simple and should focus on cleaning up only the resources that it references.
________________________________________