Back to Blog
C#

C# Finalizer: How It Works and When to Use It

c# finalizer: Understand C# finalizers: their syntax, runtime behavior, interaction with garbage collection, and when to use them for resource cleanup.

finalizergarbage collectionIDisposableresource management.NETmemory management
Illustration of a C# finalizer method being invoked by the garbage collector during memory reclamation

A C# finalizer is a method that the garbage collector invokes before reclaiming memory for an object that is no longer reachable. It provides a last chance to release unmanaged resources that the object holds. While finalizers are rarely needed in modern .NET applications, understanding their behavior is essential for correctly managing unmanaged resources and diagnosing memory issues.

The Role of a Finalizer

A finalizer is a special instance method that the garbage collector calls when it determines that an object is no longer reachable from application code. Unlike a constructor, you do not call a finalizer directly. Instead, the runtime schedules it to run as part of the garbage collection process.

The primary purpose of a finalizer is to release unmanaged resources, such as native file handles, database connections, or memory allocated via platform invocation. Managed resources are automatically reclaimed by the garbage collector, but unmanaged resources do not have that automatic cleanup. A finalizer ensures that these resources are eventually freed even if the developer forgets to call a cleanup method.

Declaring a Finalizer

In C#, a finalizer is declared using the destructor syntax, which is a tilde followed by the class name. The finalizer cannot have parameters, cannot return a value, and cannot be called explicitly.

class ResourceHolder { ~ResourceHolder() { // Clean up unmanaged resources } }

This syntax is often called a destructor, but in .NET it is a finalizer. The compiler translates this into a protected method named Finalize that overrides the System.Object.Finalize method. The runtime calls this method during garbage collection.

You should not define a finalizer in a class that only holds managed resources. Doing so adds unnecessary overhead because the object becomes finalizable, which means it must be tracked by the finalization queue and processed separately during collection.

How the Finalization Queue Works

The .NET garbage collector maintains two queues for finalizable objects: the finalization queue and the f-reachable queue. When an object with a finalizer is created, a reference to it is placed on the finalization queue. When the garbage collector runs and determines that the object is unreachable, it does not immediately reclaim the memory. Instead, it moves the reference from the finalization queue to the f-reachable queue. A dedicated finalizer thread then executes the finalizer on each object in the f-reachable queue.

After the finalizer runs, the object is considered dead and its memory can be reclaimed in a subsequent garbage collection. This means a finalizable object requires at least two garbage collection cycles to be fully collected, which delays memory reclamation and increases the workload on the garbage collector.

Because the finalizer thread runs asynchronously, there is no guarantee when the finalizer will execute. It might run shortly after the object becomes unreachable, or it might run much later, depending on system load and the frequency of garbage collections. This nondeterminism is a key reason why finalizers are not suitable for releasing scarce or time-sensitive resources.

Finalizers vs. IDisposable

For deterministic cleanup, the .NET idiom is the IDisposable interface. A class that implements IDisposable exposes a Dispose method that the caller can invoke to release resources immediately. Finalizers are a safety net for cases where Dispose is not called.

The recommended pattern is to implement IDisposable and use a finalizer only when the class directly owns unmanaged resources. The Dispose method should release both managed and unmanaged resources, and it should suppress finalization by calling GC.SuppressFinalize(this) to prevent the finalizer from running a second time.

AspectFinalizerIDisposable
DeterminismNondeterministicDeterministic
InvocationCalled by GCCalled by developer
Resource releaseUnmanaged onlyManaged and unmanaged
Performance overheadHighLow
Typical useSafety netPrimary cleanup

A finalizer should never attempt to release managed resources, because at the time it runs, those managed resources may already have been collected. Accessing them from a finalizer can cause exceptions or undefined behavior.

Common Pitfalls and Misconceptions

One common misconception is that a finalizer is a guaranteed cleanup mechanism. It is not. The finalizer is not guaranteed to run before the process exits, and it may never run if the object remains reachable until shutdown. Therefore, you cannot rely on finalizers for critical cleanup.

Another pitfall is throwing exceptions from a finalizer. If a finalizer throws an unhandled exception, the runtime treats it as an unhandled exception and terminates the process. This is because the finalizer thread cannot propagate exceptions to the application code. Always wrap finalizer logic in try-catch blocks and avoid throwing.

A third issue is object resurrection. Inside a finalizer, you can assign the current object to a static field or a global reference, making it reachable again. This is called resurrection. It is rarely needed and often causes memory leaks because the object becomes reachable and will not be finalized again unless you call GC.ReRegisterForFinalize. Avoid this pattern unless you have a very specific reason.

Performance and Operational Considerations

Finalizable objects are more expensive to allocate and collect than ordinary objects. Every allocation of a finalizable object adds a reference to the finalization queue, which the garbage collector must process. The finalizer thread adds an additional thread that runs cleanup code, which can introduce latency and contention.

In high-throughput applications, excessive use of finalizers can degrade performance and increase memory pressure. If you need to manage unmanaged resources, prefer the SafeHandle classes provided by .NET, which encapsulate native handles and implement the finalizer pattern internally. This lets you benefit from reliable cleanup without writing finalizer code yourself.

Operationally, finalizers can complicate debugging. A finalizer that runs after an object is no longer used can interact with other objects that are also being collected, leading to nondeterministic behavior. When investigating memory leaks, it is often helpful to inspect the finalization queue to see which objects are waiting for finalization.

When to Use a Finalizer

Use a finalizer only when your class directly owns an unmanaged resource that is not already wrapped by a SafeHandle. In that case, implement the full IDisposable pattern: provide a Dispose method that releases the resource, and add a finalizer as a fallback. The finalizer should call the same cleanup logic, but it must only release unmanaged resources.

For example, if you wrap a native handle that is not covered by a SafeHandle, you might write:

class NativeResource : IDisposable { private IntPtr _handle; private bool _disposed; ~NativeResource() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Release managed resources } // Release unmanaged resource _disposed = true; } }

This pattern ensures that the unmanaged resource is released deterministically when Dispose is called, and it still gets released if the caller forgets, thanks to the finalizer. In most cases, you should prefer using SafeHandle over writing this pattern manually, because SafeHandle already handles finalization and prevents handle leaks.

If you are designing a class that does not directly own unmanaged resources, do not add a finalizer. Let the garbage collector handle managed memory and rely on IDisposable for any nested resources that need deterministic cleanup.

c# finalizer: Practical Usage and Code Examples | RYUSLOG DEV