Explain garbage collection in .NET and its generations.
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:
- Mark Phase: GC identifies which objects are still in use by traversing object references from roots (static fields, local variables, CPU registers)
- Sweep Phase: GC removes unreachable objects
- 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
IDisposablefor 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