Garbage Collection in .NET Core
Garbage collection (GC) is the process by which the .NET Core runtime automatically manages memory. It reclaims memory that is no longer being used by an application, preventing memory leaks and simplifying memory management for developers. Understanding how GC works is crucial for writing efficient and stable .NET Core applications.
How Garbage Collection Works
The .NET Core garbage collector is a generational, compacting garbage collector. This means it:
- Generational: Objects are allocated into different generations. Most objects are short-lived and are allocated in the Generation 0 (Gen 0). If objects survive a Gen 0 collection, they are promoted to Generation 1 (Gen 1), and so on, up to Generation 2 (Gen 2). This approach optimizes the collection process, as Gen 0 collections are more frequent and faster because they only need to scan a small portion of the managed heap.
- Compacting: After collecting unreachable objects, the GC can move the remaining objects closer together on the heap. This fragmentation reduces and provides larger contiguous blocks of memory for future allocations, improving allocation performance.
The Managed Heap and Generations
The managed heap is where all objects allocated by the .NET Core runtime are stored. The GC divides the heap into three primary generations:
- Generation 0 (Gen 0): This is where new objects are allocated. Most objects are short-lived and will be collected in Gen 0.
- Generation 1 (Gen 1): Objects that survive a Gen 0 collection are promoted to Gen 1. This generation acts as a buffer between Gen 0 and Gen 2.
- Generation 2 (Gen 2): Objects that survive a Gen 1 collection are promoted to Gen 2. These are typically long-lived objects. A Gen 2 collection is often referred to as a "full GC" because it scans the entire managed heap.
The GC Algorithm
When a garbage collection occurs, the GC performs the following steps:
- Marking: The GC starts from the root objects (e.g., static fields, local variables on the stack, CPU registers) and traverses the object graph to find all reachable objects. Any object that can be reached from a root is considered "live."
- Relocation (Compacting): For a compacting GC, the live objects are moved to fill the gaps left by the dead objects. This process also updates references to the moved objects.
- Sweeping (or Freeing): The memory occupied by unreachable objects (which are now marked as dead) is reclaimed and made available for new allocations.
GC Modes
.NET Core supports two main GC modes:
Workstation GC
Optimized for client applications, such as desktop applications. It prioritizes low latency by running on fewer CPU cores, minimizing pauses. The Workstation GC can be configured in Server GC or Non-server GC modes. The default is Non-server GC.
- Non-server GC: Runs on a single background thread, minimizing pauses. Suitable for applications where responsiveness is critical.
- Server GC: Uses multiple background threads to perform collections concurrently on multiple CPU cores. This improves throughput by reducing the overall GC time, but may introduce slightly longer pauses. Recommended for server-side applications.
Server GC
Optimized for server applications. It aims to maximize throughput by using multiple GC threads that run on different CPU cores simultaneously. This leads to faster collections overall but can result in slightly longer pauses compared to Workstation GC.
Configuring GC Behavior
You can influence GC behavior through configuration settings:
Runtime Configuration
You can set GC modes using environment variables or a runtimeconfig.json file.
Environment Variable:
COMPlus_GCServer=1
Setting COMPlus_GCServer to 1 enables Server GC. Setting it to 0 (or not setting it) enables Workstation GC.
runtimeconfig.json:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}
Setting System.GC.Server to true enables Server GC.
GC Latency Mode
You can also control the GC's aggressiveness using the System.GC.LatencyMode property:
Batch(default): The GC tries to complete collection within a reasonable time, balancing throughput and latency.Interactive: The GC prioritizes minimizing pause times, even at the expense of throughput. Suitable for UI applications.LowLatency: Similar toInteractivebut generally more aggressive in minimizing pauses.SustainedLowLatency: Aims for consistently low pause times over extended periods.
Configuration example:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": false, // Workstation GC
"System.GC.LatencyMode": "Interactive"
}
}
}
Best Practices for GC Performance
- Avoid unnecessary allocations: Creating fewer objects means less work for the GC. Reuse objects where possible.
- Use
structsfor small, short-lived data: Value types (structs) are allocated on the stack or inline within objects, bypassing heap allocations for many cases. - Dispose of unmanaged resources promptly: Use the
usingstatement or theIDisposablepattern to ensure unmanaged resources are released, even if they are managed by a GC'd object. - Understand object lifetimes: Long-lived objects in Gen 2 can contribute to longer GC pauses.
- Consider
ArrayPool<T>: For frequent allocations of large arrays, pooling can significantly reduce GC pressure.