Skip to main content

C# Finalization

Introduction

In C# applications, memory management is largely handled automatically through the garbage collector (GC). However, when our code interacts with unmanaged resources (like file handles, database connections, or network sockets), we need additional mechanisms to ensure these resources are properly cleaned up. Finalization is C#'s built-in mechanism for providing a safety net when dealing with such resources.

This tutorial will explore the finalization process in C#, how it works alongside the garbage collector, its limitations, and best practices for resource cleanup.

What is Finalization?

Finalization is a process where objects get a "last chance" to clean up unmanaged resources before being completely removed from memory by the garbage collector. It happens through a special method called a finalizer (also known as a destructor in C# syntax).

Key Characteristics of Finalization:

  • Non-deterministic: You cannot predict exactly when finalization will occur
  • Automatic: The garbage collector handles the finalization process
  • Last resort: It's designed as a safety mechanism, not a primary cleanup strategy

Finalizers in C#

A finalizer in C# is declared using syntax that looks like a destructor from C++:

csharp
class ResourceHandler
{
// Constructor
public ResourceHandler()
{
Console.WriteLine("ResourceHandler instance created");
}

// Finalizer
~ResourceHandler()
{
Console.WriteLine("Finalizer called - cleaning up");
// Clean up unmanaged resources here
}
}

When you run code that creates and later abandons an instance of this class:

csharp
static void Main()
{
CreateAndAbandonObject();

// Force garbage collection to demonstrate finalization
// (Don't do this in real applications)
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine("Program ending");
}

static void CreateAndAbandonObject()
{
var handler = new ResourceHandler();
// handler goes out of scope here
}

Output:

ResourceHandler instance created
Finalizer called - cleaning up
Program ending

How Finalization Works

Understanding the finalization process helps you make better decisions about resource management:

  1. When an object with a finalizer is created, it's registered in a special finalization queue
  2. When the GC determines the object is unreachable, it moves it to the "freachable" queue (f-reachable: finalizable but reachable from the finalization queue)
  3. A special finalization thread processes this queue, calling the finalizers
  4. After finalization, the object becomes eligible for collection in the next GC cycle

This two-phase process means finalization adds overhead and delays actual memory reclamation.

Limitations of Finalization

Finalizers come with several significant limitations that you should be aware of:

1. Non-deterministic Timing

You can't predict when a finalizer will run. It might be immediate, or it might be delayed until application shutdown.

csharp
static void DemonstrateNonDeterminism()
{
for (int i = 0; i < 10000; i++)
{
var resource = new ResourceHandler();
// Objects are abandoned but finalization timing is unpredictable
}

Console.WriteLine("Loop completed");
// You might or might not see finalizer messages by this point
}

2. Performance Impact

Objects with finalizers:

  • Take longer to allocate (they must be registered)
  • Take at least two garbage collection cycles to be fully collected
  • Require more CPU time for the garbage collector

3. No Guaranteed Execution

Finalizers don't run if:

  • The program terminates abnormally (e.g., crash or power failure)
  • The finalizer thread is blocked or terminated
  • Environment.FailFast() is called

4. Order of Finalization

You can't rely on the order in which finalizers run, which can be problematic when objects have dependencies on each other.

Best Practices for Finalization

Given the limitations of finalization, here are some best practices:

1. Use the Dispose Pattern

The recommended approach for handling resources is the dispose pattern, using finalizers only as a backup:

csharp
public class ManagedFileHandle : IDisposable
{
private IntPtr _handle; // Unmanaged resource
private bool _disposed = false;

public ManagedFileHandle(string filename)
{
// Open file and get handle
_handle = OpenFile(filename);
}

// Implement IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Clean up managed resources
}

// Clean up unmanaged resources
if (_handle != IntPtr.Zero)
{
CloseFile(_handle);
_handle = IntPtr.Zero;
}

_disposed = true;
}
}

// Finalizer as a safety net
~ManagedFileHandle()
{
Dispose(false);
}

// Simulated native methods
private IntPtr OpenFile(string filename)
{
Console.WriteLine($"Opening file: {filename}");
return new IntPtr(1); // Simulate valid handle
}

private void CloseFile(IntPtr handle)
{
Console.WriteLine($"Closing file handle: {handle}");
}
}

2. Using the using Statement

The using statement ensures proper resource disposal:

csharp
public static void ReadFile(string path)
{
using (var file = new ManagedFileHandle(path))
{
// Work with file
Console.WriteLine("Working with file...");
} // file.Dispose() is called automatically here

Console.WriteLine("File has been properly disposed");
}

Output:

Opening file: example.txt
Working with file...
Closing file handle: 1
File has been properly disposed

3. Avoiding Complex Operations in Finalizers

Keep finalizers simple and focused on releasing unmanaged resources:

csharp
// Bad practice
~BadFinalizer()
{
// DON'T do this in finalizers
try
{
Database.LogCleanupActivity(); // Might access disposed objects
Thread.Sleep(1000); // Delays other finalizations
var data = FetchConfigFromNetwork(); // Might fail unpredictably
}
catch (Exception ex)
{
Console.WriteLine(ex); // Might be ignored in finalization
}
}

// Good practice
~GoodFinalizer()
{
if (_handle != IntPtr.Zero)
{
ReleaseHandle(_handle);
_handle = IntPtr.Zero;
}
}

Real-World Example: Database Connection Wrapper

Here's a practical example showing how to properly implement finalization and disposal for a database connection wrapper:

csharp
public class DatabaseConnection : IDisposable
{
private SqlConnection _connection;
private bool _disposed = false;

public DatabaseConnection(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
Console.WriteLine("Database connection opened");
}

public void ExecuteQuery(string query)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DatabaseConnection));

Console.WriteLine($"Executing query: {query}");
// Implementation omitted
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
if (_connection != null)
{
_connection.Close();
_connection.Dispose();
Console.WriteLine("Database connection closed and disposed");
}
}

// No unmanaged resources to clean up in this example

_disposed = true;
}
}

~DatabaseConnection()
{
Dispose(false);
}
}

Usage:

csharp
public static void ProcessData()
{
using (var db = new DatabaseConnection("Server=myserver;Database=mydb;"))
{
db.ExecuteQuery("SELECT * FROM Customers");
db.ExecuteQuery("UPDATE Orders SET Status = 'Shipped'");
} // db.Dispose() called automatically
}

Understanding the Finalization Queue and F-Reachable Queue

To better understand the garbage collection process with finalizable objects:

csharp
static void ExplainFinalizationQueues()
{
Console.WriteLine($"Initial memory: {GC.GetTotalMemory(true)} bytes");

// Create many objects with finalizers
var list = new List<ResourceHandler>();
for (int i = 0; i < 10000; i++)
{
list.Add(new ResourceHandler());
}

Console.WriteLine($"After creating objects: {GC.GetTotalMemory(false)} bytes");

// Clear references to objects
list.Clear();
list = null;

Console.WriteLine("References cleared");
Console.WriteLine($"Before collection: {GC.GetTotalMemory(false)} bytes");

// First collection - moves objects to F-reachable queue
GC.Collect();
Console.WriteLine($"After first collection: {GC.GetTotalMemory(false)} bytes");

// Wait for finalizers to run
GC.WaitForPendingFinalizers();
Console.WriteLine("Finalizers have run");

// Second collection - now objects can be reclaimed
GC.Collect();
Console.WriteLine($"After second collection: {GC.GetTotalMemory(true)} bytes");
}

Summary

Finalization in C# provides a safety mechanism for cleaning up unmanaged resources when objects are garbage collected. However, it comes with significant limitations:

  • The timing is non-deterministic
  • It adds overhead to garbage collection
  • It's not guaranteed to run in all scenarios

For these reasons, best practices include:

  1. Implementing the IDisposable pattern for deterministic cleanup
  2. Using finalizers only as a safety net for unmanaged resources
  3. Using the using statement for proper resource disposal
  4. Keeping finalizer code simple and focused only on releasing unmanaged resources

By understanding finalization and following these best practices, you can write more robust C# applications that properly manage both managed and unmanaged resources.

Further Exercises

  1. Create a class that wraps a file handle (using System.IO.FileStream) and implement both proper disposal and finalization
  2. Experiment with the timing of finalization by creating many objects with finalizers and observing when they get finalized
  3. Implement a resource pool that manages limited resources and uses proper disposal patterns
  4. Compare the memory usage and performance between classes with and without finalizers

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)