Lessons in this course (11)

6. The garbage collector in depth — generations, LOH, POH and GC modes

Intermediate★ Must knowLesson 6 of 11

Allocation: the GC hands each thread an allocation context; new just bumps a pointer — allocation is very cheap.

Collection — mark, (sweep), compact:

  1. Suspend managed threads at safe points (for blocking GCs).
  2. Mark — starting from roots (stack slots and registers, static fields, GC handles, finalization queue), follow references and mark reachable objects.
  3. Plan/compact — slide surviving objects together, update all references; free space becomes one contiguous block (no fragmentation). Sometimes it only sweeps (builds a free list) when compaction isn't worth it.
  4. Resume threads.

Generations (the "weak generational hypothesis": most objects die young):

GenerationHoldsCollected
Gen 0Newly allocated small objectsVery often, very fast (ms or less)
Gen 1Survivors of one gen-0 GC — buffer between young and oldOften
Gen 2Long-lived objects (caches, singletons, static data)Rarely; a "full GC" — most expensive
LOH — Large Object HeapObjects ≥ 85,000 bytes (big arrays, big strings)Only with gen 2; not compacted by default (can fragment)
POH — Pinned Object Heap (.NET 5+)Objects allocated as pinned (GC.AllocateArray(..., pinned: true))With gen 2, never moved

Objects that survive a collection are promoted to the next generation. Gen-2 objects referencing gen-0 objects are tracked with a card table so gen-0 GCs don't have to scan all of gen 2.

GC modes:

ModeBehaviourDefault for
WorkstationOne heap; collections on the triggering thread; low pause, less throughputDesktop/console apps
ServerOne heap + GC thread per core; higher throughput, more memoryASP.NET Core apps
Concurrent / Background GCGen-2 marking runs concurrently with the app; gen 0/1 still pause brieflyOn by default
DATAS (dynamic adaptation, .NET 8 opt-in, on by default with Server GC in .NET 9)Server GC that grows/shrinks heap count to the load — less memory for small containersContainers
xml
<!-- .csproj -->
<PropertyGroup>
  <ServerGarbageCollector>true</ServerGarbageCollector>
  <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
csharp
Console.WriteLine(GC.CollectionCount(0) + " gen0, " + GC.CollectionCount(2) + " gen2");
Console.WriteLine(GC.GetTotalMemory(forceFullCollection: false) / 1_048_576 + " MB");
var info = GC.GetGCMemoryInfo();   // heap size, fragmentation, pause durations
// GC.Collect();                   // almost never call this in production code

What triggers a GC: gen-0 budget exhausted (most common), GC.Collect(), low memory notification from the OS, LOH allocation thresholds, container memory limits.