Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A finalizer is a special, non-deterministic method invoked by the .NET Garbage Collector (GC) to perform cleanup operations before an object’s memory is reclaimed. It serves as a fallback mechanism to ensure unmanaged resources are released if explicit disposal does not occur.

Syntax and Compilation

A finalizer is declared using a tilde (~) followed by the class name. It cannot have access modifiers, takes no parameters, and cannot be overloaded. Finalizers are restricted to reference types (class or record class); they cannot be defined on value types (struct).
public class ResourceHandler
{
    // Finalizer declaration
    ~ResourceHandler()
    {
        // Unmanaged cleanup logic executes here
    }
}
At compile time, the C# compiler translates the finalizer syntax into an override of the System.Object.Finalize() method. It automatically wraps the finalizer body in a try-finally block to ensure the base class finalizer is always called. Because explicitly overriding Finalize() or calling base.Finalize() is strictly forbidden in C# and results in compiler errors, the compiler generates Intermediate Language (IL) that is logically equivalent to the following pseudo-code:
// Pseudo-code representing compiler-generated IL
protected override void Finalize()
{
    try
    {
        // Unmanaged cleanup logic executes here
    }
    finally
    {
        base.Finalize();
    }
}

Execution Mechanics and the CLR

Finalizer execution is entirely managed by the Common Language Runtime (CLR) and is non-deterministic, meaning you cannot predict exactly when it will run. The lifecycle of an object with a finalizer involves specific internal CLR data structures:
  1. Allocation and the Finalization Queue: When an object implementing a finalizer is instantiated, the CLR allocates the object on the managed heap and places a pointer to it in an internal structure called the Finalization Queue.
  2. First GC Pass (Marking): When the GC performs a collection, it identifies unreachable objects. If an unreachable object is found in the Finalization Queue, its memory is not immediately reclaimed.
  3. The F-Reachable Queue: The GC moves the object’s pointer from the Finalization Queue to the F-Reachable (Freachable) Queue. At this point, the object is temporarily “resurrected” because the F-Reachable queue acts as a GC root, holding a strong reference to it.
  4. Finalizer Thread Execution: A dedicated, high-priority CLR background thread monitors the F-Reachable queue. When populated, this thread dequeues the objects and executes their finalizer methods sequentially.
  5. Second GC Pass (Reclamation): Once the finalizer completes, the object is removed from the F-Reachable queue. It is now truly unreachable. During the next garbage collection cycle that covers the object’s generation, its memory is finally reclaimed.
Application Termination Semantics: In modern .NET (.NET Core and .NET 5+), finalizers are not invoked during application termination (process exit). This is a major behavioral departure from the legacy .NET Framework, where the CLR attempted to run all pending finalizers on shutdown. Developers must be aware of this semantic shift and never rely on finalizers for guaranteed end-of-process cleanup.

Critical Restrictions and Safety

Writing finalizers requires strict adherence to safety rules due to the environment in which the finalizer thread operates:
  • No Managed Object References: Finalizers must never reference or call methods on other managed objects. When an object is moved to the F-Reachable queue, that queue acts as a GC root. This keeps the finalizable object and its entire referenced object graph alive in memory. Therefore, the memory of referenced managed objects is not reclaimed at this stage. However, because the GC does not guarantee the order of finalization, the finalizers of those referenced objects may have already executed, leaving them in an invalid or disposed state. Accessing them can lead to unpredictable behavior or ObjectDisposedException.
  • Fatal Exceptions: Unhandled exceptions thrown within a finalizer will immediately terminate the application process. They bypass standard application exception handling mechanisms. All logic within a finalizer must be strictly defensive and fail-safe.

Performance Implications

Because objects with finalizers require at least two garbage collection cycles to be fully destroyed, they inherently incur a performance penalty. During the first GC pass, the object survives collection and is consequently promoted to the next GC generation (e.g., from Generation 0 to Generation 1). This promotion extends the object’s lifespan significantly, as higher generations are collected much less frequently. Furthermore, the finalizer thread operates sequentially; a blocked or infinitely looping finalizer will halt the execution of all subsequent finalizers in the queue, potentially leading to memory leaks and application crashes.

Modern Alternative: SafeHandle

In modern .NET development, writing custom finalizers is largely obsolete. Microsoft strongly recommends using System.Runtime.InteropServices.SafeHandle to wrap unmanaged resources instead of implementing custom finalizers. SafeHandle provides guaranteed execution and protects against asynchronous exceptions (such as thread aborts or out-of-memory conditions) that could otherwise interrupt finalization and corrupt state. By delegating unmanaged resource management to a SafeHandle, developers can avoid the complexities, performance penalties, and safety risks associated with custom finalizers.

The Standard Dispose Pattern

When a custom finalizer is strictly necessary, it is idiomatically implemented alongside the IDisposable interface using the standard Dispose(bool disposing) pattern. This pattern provides a centralized mechanism to safely separate managed resource cleanup (which is safe only during explicit disposal) from unmanaged resource cleanup (which is safe during both explicit disposal and finalization). When explicit disposal succeeds, GC.SuppressFinalize is called to remove the object from the Finalization Queue, bypassing the F-Reachable queue entirely and avoiding the finalization performance penalty.
public class ResourceHandler : IDisposable
{
    private bool _disposed = false;

    // Finalizer acts as the fallback
    ~ResourceHandler()
    {
        // False indicates execution from the finalizer thread
        Dispose(disposing: false);
    }

    // Explicit deterministic cleanup
    public void Dispose()
    {
        // True indicates execution from user code
        Dispose(disposing: true);
        
        // Instruct the GC to bypass the finalizer
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // SAFE: Clean up other MANAGED objects here.
            // This block is skipped during finalization.
        }

        // SAFE: Clean up UNMANAGED resources here.
        // This executes during both explicit Dispose() and finalization.

        _disposed = true;
    }
}
Master C# with Deep Grasping Methodology!Learn More