Explain garbage collection in .NET and its generations.

4 minintermediate.NETgarbage-collectionmemoryperformance

Quick Answer

Garbage collection is .NET's automatic memory manager: it tracks object references from roots, then marks live objects, sweeps unreachable ones, and compacts the heap. To make this efficient it's generational — Gen 0 (short-lived, collected most often), Gen 1 (a buffer), and Gen 2 (long-lived), plus the Large Object Heap. Most objects die young in Gen 0, so collecting it is cheap; survivors are promoted to higher generations that are collected less frequently.

Detailed Answer

Garbage Collection (GC) is .NET's automatic memory management system that reclaims memory occupied by unused objects.

How Garbage Collection Works:

  1. Mark Phase: GC identifies which objects are still in use by traversing object references from roots (static fields, local variables, CPU registers)
  2. Sweep Phase: GC removes unreachable objects
  3. Compact Phase: GC moves surviving objects together to reduce fragmentation

GC Generations:

.NET uses a generational garbage collection model with three generations:

Generation 0 (Gen 0) - Young objects
├── Short-lived objects
├── Temporary variables
├── Fast collection (< 1ms typically)
└── Most frequent collections

Generation 1 (Gen 1) - Middle-aged objects
├── Survived one Gen 0 collection
├── Buffer between Gen 0 and Gen 2
├── Fast collection
└── Intermediate frequency

Generation 2 (Gen 2) - Old objects
├── Long-lived objects
├── Static data
├── Slower collection (can be 100ms+)
└── Least frequent collections

Large Object Heap (LOH)
├── Objects > 85,000 bytes
├── Not compacted by default
├── Collected with Gen 2
└── Can cause fragmentation

Generation Promotion:

// Example of object lifetime
public void DemonstrateGenerations()
{
    // Gen 0 - temporary object
    var temp = new byte[100];
    
    // Gen 0 collection happens
    GC.Collect(0);
    
    // 'temp' survives, promoted to Gen 1
    
    // More Gen 0 collections...
    GC.Collect(0);
    
    // 'temp' still alive, promoted to Gen 2
    
    Console.WriteLine($"Generation: {GC.GetGeneration(temp)}");
}

GC Modes:

1. Workstation GC

  • For client applications
  • Optimized for responsiveness
  • Runs on same thread that triggered collection
// In .csproj

  false

2. Server GC

  • For server applications
  • Optimized for throughput
  • Multiple GC threads (one per CPU)
  • Larger heap segments
// In .csproj

  true

GC Collection Types:

// Check GC mode
bool isServerGC = GCSettings.IsServerGC;
Console.WriteLine($"Server GC: {isServerGC}");

// Check latency mode
Console.WriteLine($"Latency Mode: {GCSettings.LatencyMode}");

// Set latency mode
GCSettings.LatencyMode = GCLatencyMode.LowLatency; // For time-critical operations

// Force garbage collection (avoid in production!)
GC.Collect(); // Full collection
GC.Collect(0); // Gen 0 only
GC.Collect(2, GCCollectionMode.Optimized);

// Wait for finalization
GC.WaitForPendingFinalizers();

// Get GC stats
for (int i = 0; i <= GC.MaxGeneration; i++)
{
    Console.WriteLine($"Gen {i} collections: {GC.CollectionCount(i)}");
}

GC Notifications:

// Register for GC notifications
GC.RegisterForFullGCNotification(10, 10);

// In a monitoring thread
while (true)
{
    GCNotificationStatus status = GC.WaitForFullGCApproach();
    if (status == GCNotificationStatus.Succeeded)
    {
        Console.WriteLine("Full GC is approaching...");
        // Prepare: redirect traffic, pause operations, etc.
    }
    
    status = GC.WaitForFullGCComplete();
    if (status == GCNotificationStatus.Succeeded)
    {
        Console.WriteLine("Full GC completed.");
        // Resume normal operations
    }
}

GC Performance Tips:

// 1. Reuse objects when possible
private static readonly StringBuilder _builder = new StringBuilder();

public string BuildString(params string[] parts)
{
    _builder.Clear();
    foreach (var part in parts)
        _builder.Append(part);
    return _builder.ToString();
}

// 2. Use object pooling
private static readonly ObjectPool _pool = 
    new DefaultObjectPool(new StringBuilderPooledObjectPolicy());

public string BuildStringPooled(params string[] parts)
{
    var builder = _pool.Get();
    try
    {
        foreach (var part in parts)
            builder.Append(part);
        return builder.ToString();
    }
    finally
    {
        _pool.Return(builder);
    }
}

// 3. Use structs for small, short-lived data
public struct Point // Value type, stack allocated
{
    public int X { get; set; }
    public int Y { get; set; }
}

// 4. Avoid finalizers unless necessary
public class ResourceHolder : IDisposable
{
    private bool _disposed;
    
    public void Dispose()
    {
        if (!_disposed)
        {
            // Clean up managed resources
            _disposed = true;
            GC.SuppressFinalize(this); // Prevent finalizer call
        }
    }
    
    // Only add finalizer if holding unmanaged resources
    ~ResourceHolder()
    {
        Dispose();
    }
}

GC Pressure:

// Add memory pressure for unmanaged resources
public class UnmanagedResourceHolder : IDisposable
{
    private IntPtr _unmanagedMemory;
    private const long ResourceSize = 1024 * 1024; // 1MB
    
    public UnmanagedResourceHolder()
    {
        _unmanagedMemory = Marshal.AllocHGlobal((int)ResourceSize);
        GC.AddMemoryPressure(ResourceSize); // Tell GC about unmanaged memory
    }
    
    public void Dispose()
    {
        if (_unmanagedMemory != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_unmanagedMemory);
            GC.RemoveMemoryPressure(ResourceSize);
            _unmanagedMemory = IntPtr.Zero;
        }
    }
}

Weak References:

// Hold reference without preventing collection
public class CacheManager
{
    private readonly Dictionary> _cache = new();
    
    public void AddToCache(string key, byte[] data)
    {
        _cache[key] = new WeakReference(data);
    }
    
    public byte[] GetFromCache(string key)
    {
        if (_cache.TryGetValue(key, out var weakRef) && 
            weakRef.TryGetTarget(out var data))
        {
            return data; // Object still alive
        }
        return null; // Object was collected
    }
}

Best Practices:

  • Don't call GC.Collect() manually in production
  • Use IDisposable for deterministic cleanup
  • Minimize allocations in hot paths
  • Use structs for small, short-lived data
  • Pool objects for frequently allocated types
  • Monitor GC metrics in production