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++:
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:
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:
- When an object with a finalizer is created, it's registered in a special finalization queue
- When the GC determines the object is unreachable, it moves it to the "freachable" queue (f-reachable: finalizable but reachable from the finalization queue)
- A special finalization thread processes this queue, calling the finalizers
- 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.
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:
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:
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:
// 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:
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:
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:
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:
- Implementing the IDisposable pattern for deterministic cleanup
- Using finalizers only as a safety net for unmanaged resources
- Using the
using
statement for proper resource disposal - 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
- Create a class that wraps a file handle (using System.IO.FileStream) and implement both proper disposal and finalization
- Experiment with the timing of finalization by creating many objects with finalizers and observing when they get finalized
- Implement a resource pool that manages limited resources and uses proper disposal patterns
- Compare the memory usage and performance between classes with and without finalizers
Additional Resources
- Microsoft Docs: Cleaning Up Unmanaged Resources
- Microsoft Docs: Implementing a Dispose Method
- C# in Depth by Jon Skeet - Contains excellent explanations of finalization and disposal
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)