How .NET Garbage Collection Works in C
The .NET Garbage Collector (GC) provides automatic memory management for C# applications by allocating resources on the managed heap and reclaiming memory from objects that are no longer in use. This article covers the fundamental mechanics of the .NET GC, detailing how memory is allocated via the managed heap, the role of object generations, the phases of memory deallocation, and how large objects are handled.
The Managed Heap and Memory Allocation
When a C# application initializes, the runtime reserves a contiguous region of address space known as the managed heap. This heap is divided into distinct segments: the Small Object Heap (SOH), the Large Object Heap (LOH), and, in modern .NET versions, the Pinned Object Heap (POH).
Memory allocation on the Small Object Heap is exceptionally fast. The runtime maintains an allocation pointer pointing to the next available address in memory:
- Instantiation: When you instantiate a reference
type using the
newkeyword, the runtime checks if the object fits in the remaining space. - Pointer Advancement: If sufficient space exists, the object is placed at the current pointer address, and the pointer advances by the size of the object. This behaves similarly to allocating memory on the stack.
- Threshold Check: If an object is 85,000 bytes or larger, it bypasses the standard path and is allocated directly onto the Large Object Heap (LOH).
Generational Memory Architecture
To optimize performance, the .NET GC classifies objects into three generations based on their lifespan:
- Generation 0 (Gen 0): Contains newly allocated, short-lived objects such as local variables and temporary collections. Gen 0 is small and collected frequently.
- Generation 1 (Gen 1): Acts as a buffer between short-lived and long-lived objects. Objects that survive a Gen 0 collection are promoted to Gen 1.
- Generation 2 (Gen 2): Contains long-lived objects, such as static data, application-wide singletons, and objects surviving Gen 1. The Large Object Heap is also collected alongside Gen 2 (often called a full GC).
Triggering Garbage Collection
Garbage collection runs automatically when specific criteria are met:
- Generation 0 reaches its allocation threshold.
- The system signals low physical memory pressure from the operating system.
- The application explicitly calls
GC.Collect(), which is generally discouraged except in specific profiling or cleanup scenarios.
The Phases of Garbage Deallocation
When a collection occurs, the GC halts application threads (unless background GC is active) and executes three primary phases:
- Marking Phase: The GC creates a graph of all active objects. It begins from a set of "roots," which include active stack references, CPU registers, static variables, and GC handles. The GC traverses these roots recursively, marking every reachable object as alive.
- Sweeping Phase: Any object on the heap that was not marked during the traversal is identified as dead (unreachable) memory.
- Compacting Phase: For the Small Object Heap, the GC shifts surviving objects together toward the start of the memory segment. This eliminates dead space, prevents heap fragmentation, and restores a contiguous block of free space for future bump allocations. Object references are automatically updated to reflect the new addresses.
Large Object Heap (LOH) Considerations
Because moving massive memory blocks degrades performance, the GC
typically sweeps the Large Object Heap without compacting it. Surviving
large objects remain in place, which can cause fragmentation over time
if large objects of varying sizes are continually created and destroyed.
In modern .NET, developers can configure on-demand LOH compaction via
GCSettings.LargeObjectHeapCompactionMode.
Resource Cleanup: Finalization and IDisposable
The GC only manages managed memory; unmanaged resources (such as file handles, database connections, and OS handles) require explicit cleanup.
- Finalizers (
~ClassName): Code that executes before the GC reclaims an object. Objects with finalizers must survive at least one extra collection cycle because they are placed in a finalization queue, delaying their reclamation. - The Dispose Pattern (
IDisposable): Provides a deterministic way to release unmanaged resources immediately via theDispose()method orusingstatements, allowing the object to suppress its finalizer viaGC.SuppressFinalize(this)and avoid GC retention delays.