Garbage Collection, Finalizers, and Disposal in C#
Memory Allocation: Stack vs. Heap
Data is stored in memory, which is divided into Stack and Heap segments.
| Stack Memory | Heap Memory |
|---|---|
| Fast, efficient access | Complex structure |
| Limited size and types | Stores objects |
| Holds simple data | Stores reference types |
| Stores primitive and value types | — |
Consider this example:
var customer = new Customer
{
Id = 123,
Name = "Jack",
Address = "Zhuhai"
};
In heap memory, the object's data is stored:
| Address (e.g., #1000) | Id: 123 | Name: "Jack" | Address: "Zhuhai" |
On the stack, only a reference (pointer) is stored: customerRef = 1000.
If the stack reference is deleted first, the corresponding heap data becomes unreachable and cannot be freed, leading to a memory leak. Languages like C# and Java introduce automatic garbage collection (GC) to manage this, allowing developers to focus on object logic.
An object becomes eligible for collection when its reference count drops to zero.
How the Garbage Collector Operates
Garbage collection is computationally expensive, requiring traversal of object graphs. To improve efficiency, a Generational model is used, grouping objects into three categories:
| Generation 0 | Generation 1 | Generation 2 |
|---|---|---|
| Short-lived objects | Mid-term objects | Long-lived objects |
| Checked every GC cycle | Checked less frequently | Checked occasionally |
When memory pressure is high, GC may collect objects from all generations.
GC Responsibilities Extend Beyond Simple Cleanup
- Identifies and marks dead objects in the heap.
- Compacts memory to eliminate fragmentation, improving allocation and access speed. Note: Very large memory segments are not compacted.
GC Runs on a Dedicated Thread
- Executes on a sepraate background thread.
- Each collection cycle consumes computational resources.
- The system is designed to minimize collection frequency and maximize efficiency.
Finalizers and the IDisposable Pattern
A finalizer (historically called a destructor) is used to perform necessary cleanup before an object is reclaimed by the garbage collector. In most cases, writing finalizers is unnecessary if unmanaged resources are wrapped using
System.Runtime.InteropServices.SafeHandleor a derived class.
Avoid using finalizers unless absolutely required, as they incur performance overhead.
Example:
public class ResourceHolder
{
public ResourceHolder()
{
Console.WriteLine("ResourceHolder instance created.");
}
~ResourceHolder() // Finalizer
{
Console.WriteLine("ResourceHolder instance finalized.");
}
}
class Program
{
static void Main(string[] args)
{
var holder = new ResourceHolder();
Console.WriteLine("End of Main method.");
// The object is still in scope here, so the finalizer likely won't run yet.
}
}
Running this code typically will not display the finalizer's output. This is because the object remains reachable (in scope) until the Main method exits. The garbage collector runs at an indeterminate time after the object becomes unreachable.
To potentially observe the finalizer running, limit the object's scope:
class Program
{
static void CreateAndRelease()
{
var holder = new ResourceHolder();
} // 'holder' goes out of scope here
static void Main(string[] args)
{
CreateAndRelease();
// Suggest garbage collection (for demonstration only).
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("End of Main method.");
}
}
For deterministic cleanup of unmanaged resources (like file handles or database connections), implement the IDisposable pattern:
public class ManagedResource : IDisposable
{
private bool _disposed = false;
private System.IO.Stream _fileStream; // Example unmanaged resource
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Finalizer not needed if already disposed
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Release managed resources
_fileStream?.Close();
}
// Release unmanaged resources here
_disposed = true;
}
}
~ManagedResource() // Finalizer as a safety net
{
Dispose(false);
}
}
// Usage
using (var resource = new ManagedResource())
{
// Work with resource
} // Dispose() is called automatically here