100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

Garbage Collection in .NET

How the .NET runtime automatically tracks and reclaims managed memory using a generational, tracing garbage collector, and how to work with it instead of against it.

PerformanceIntermediate10 min readJul 10, 2026
Analogies

How the .NET Garbage Collector Works

The .NET garbage collector (GC) is a tracing, generational, mostly-compacting memory manager built into the CLR. Instead of you calling free() or delete, the CLR tracks every managed object reachable from a set of roots (static fields, local variables on the stack, CPU registers, and GC handles) and reclaims anything that is no longer reachable. A collection walks the object graph from these roots, marks everything still reachable, and then reclaims the rest, optionally compacting the heap by sliding live objects together to eliminate fragmentation.

🏏

Cricket analogy: Like a scorer cross-checking the players still on the field against the official team sheet, the GC walks from its roots and marks every object still reachable, exactly as a scorer would flag anyone not on the current XI for removal from the live count.

Generations: Gen0, Gen1, Gen2

The GC is generational because empirical data shows most objects die young. New objects are allocated in Generation 0, a small, fast-to-collect segment. Objects that survive a Gen0 collection are promoted to Gen1, which acts as a buffer between short-lived and long-lived data, and objects surviving Gen1 collections are promoted to Gen2, which holds long-lived objects like caches and singletons. Because Gen0 collections are cheap (they only scan a small, recently-allocated region), the CLR can run them frequently without pausing the application for long, while full Gen2 collections are rarer and more expensive since they scan the whole heap.

🏏

Cricket analogy: It's like a franchise's age-group pathway in the IPL: promising Under-19 players (Gen0) who keep performing get promoted to the state team (Gen1), and only the proven few graduate to the senior IPL squad (Gen2), mirroring how survivors get promoted between generations.

Server GC vs Workstation GC

The CLR ships two GC flavors, configurable via the ServerGarbageCollection setting in the project's runtimeconfig or .csproj. Workstation GC runs on a single dedicated GC thread and is tuned for low-latency, interactive apps like WPF or WinForms clients where you don't want long pauses stealing UI responsiveness. Server GC creates one heap and one dedicated GC thread per logical CPU core, favoring throughput over latency, and is the default for ASP.NET Core apps because web servers usually have many cores and care more about overall requests-per-second than any single collection's pause time. Concurrent (background) GC can run alongside either mode to reduce blocking pauses for Gen2 collections.

🏏

Cricket analogy: It's like choosing between a single specialist net bowler (Workstation GC) working one lane versus opening every net lane at the academy simultaneously (Server GC) so the whole squad, not just one batter, gets through practice faster.

Large Object Heap (LOH)

Objects 85,000 bytes or larger (a threshold checked per-allocation, not per-type) go straight onto the Large Object Heap instead of Gen0. The LOH is logically part of Gen2 for collection purposes, but historically was not compacted, so long-running services that repeatedly allocate large arrays or buffers could suffer LOH fragmentation, where free gaps between surviving large objects can't be reused for a smaller allocation that doesn't fit. Since .NET Core 3.0+ you can opt into LOH compaction on demand via GCSettings.LargeObjectHeapCompactionMode, and pooling large buffers with System.Buffers.ArrayPool<T> is the standard way to avoid LOH churn entirely in hot paths like image processing or network buffer handling.

🏏

Cricket analogy: It's like a stadium reserving the biggest hospitality boxes (LOH) separately from general seating, and if a box holder leaves mid-series, that oddly-shaped gap can sit empty since a regular ticket group doesn't fit it, just like LOH fragmentation.

csharp
// Demonstrates generational promotion and LOH pooling awareness
public class BufferProcessor
{
    // Buffers >= 85,000 bytes go on the Large Object Heap.
    private static readonly ArrayPool<byte> Pool = ArrayPool<byte>.Shared;

    public void ProcessLargeFrame(int size)
    {
        byte[] buffer = Pool.Rent(size); // avoids repeated LOH allocations
        try
        {
            FillFrame(buffer, size);
        }
        finally
        {
            Pool.Return(buffer, clearArray: true);
        }
    }

    public void InspectHeap()
    {
        Console.WriteLine($"Gen0 collections: {GC.CollectionCount(0)}");
        Console.WriteLine($"Gen1 collections: {GC.CollectionCount(1)}");
        Console.WriteLine($"Gen2 collections: {GC.CollectionCount(2)}");
        Console.WriteLine($"Total managed memory: {GC.GetTotalMemory(false)} bytes");
    }

    private void FillFrame(byte[] buffer, int size) { /* fill logic */ }
}

Calling GC.Collect() manually in production code is almost always a mistake: it forces an expensive full collection, defeats the generational heuristics the runtime tuned for you, and can actually increase overall pause time. Reserve it for narrow diagnostic scenarios, not application logic.

You can inspect real GC behavior with dotnet-counters monitor --process-id <pid> System.Runtime to watch gen-0/1/2 collection counts, heap size, and time-in-GC live, which is far more reliable than guessing from symptoms alone.

  • The GC is a tracing collector that reclaims objects unreachable from roots like statics, stack locals, and GC handles.
  • Objects are allocated in Gen0 and promoted to Gen1 then Gen2 if they survive collections, based on the generational hypothesis that most objects die young.
  • Workstation GC favors low latency for client apps; Server GC favors throughput and is the ASP.NET Core default, using one heap and thread per core.
  • Objects 85,000 bytes or larger go on the Large Object Heap, which is logically part of Gen2 and historically prone to fragmentation.
  • ArrayPool<T> lets you rent and return large buffers to avoid repeated LOH allocations in hot paths.
  • Avoid calling GC.Collect() manually in production; let the generational heuristics do their job.
  • Tools like dotnet-counters and dotnet-trace give real visibility into GC behavior instead of guesswork.

Practice what you learned

Was this page helpful?

Topics covered

#NET#NETCoreStudyNotes#MicrosoftTechnologies#GarbageCollectionInNET#Garbage#Collection#Collector#Works#StudyNotes#SkillVeris