.NET Academy

The garbage collector

How .NET finds and frees unused objects, why it uses generations, and how memory still leaks.

12 min read

You will learn

  • Explain reachability and GC roots
  • Describe generations 0, 1 and 2 and the large object heap
  • Know what finalizers are and why to avoid them
  • Recognize common memory leaks in managed code

You allocate, the runtime frees

In C you must free everything you malloc — forget once and you leak, free twice and you crash. In .NET you just create objects; the garbage collector (GC) reclaims memory from objects you no longer use. That's a big part of what "managed code" means.

How the GC decides what's garbage

The GC doesn't count references. Instead, when it runs, it traces:

  1. It starts from the roots: static fields, local variables and parameters of methods currently running on any thread, and a few internal handles.
  2. It follows every reference from those roots, marking each object it reaches as alive.
  3. Everything it didn't reach is garbage, and its memory is reclaimed. Surviving objects may be compacted (moved together) to avoid fragmentation.

So an object becomes collectable as soon as nothing reachable points to it — even if it was used a millisecond ago. And setting a variable to null doesn't free anything by itself; it just removes one path.

Generations

Scanning the whole heap every time would be slow. The GC exploits a simple observation: most objects die young (temporary strings, LINQ iterators, request DTOs), while objects that survive a while tend to live long (caches, singletons, configuration).

GenerationContainsCollected
Gen 0Newly allocated objectsVery often, very fast
Gen 1Objects that survived one collection — a bufferLess often
Gen 2Long-lived objectsRarely (a "full" collection)
LOHLarge objects (≥ 85,000 bytes), logically part of Gen 2With Gen 2

Objects that survive a collection are promoted to the next generation. Try it:

Interactive

Garbage collector

Allocate objects, drop references, and run collections

Gen 0

2/8 · collected 0×

New objects. Collected often & fast.

Gen 1

collected 0×

Survived one GC. A buffer.

Gen 2

collected 0×

Long-lived. Collected rarely.

reachable (click to drop its reference) garbageObjects freed so far: 0
  • Long-lived objects (config, logger, cache) already sit in older generations.

Watch how temporary objects disappear in cheap Gen 0 collections, while long-lived ones settle into Gen 2 and stop being re-checked constantly.

Pauses and modes

Collections may briefly pause your threads. .NET minimizes this with background GC (Gen 2 collections run concurrently) and two main flavours:

  • Workstation GC — tuned for responsiveness; the default for desktop apps.
  • Server GC — one heap per CPU core, tuned for throughput; the default for ASP.NET Core apps.

Finalizers

A class can declare a finalizer (~MyClass() { ... }) that the GC runs before reclaiming the object. Finalizers exist for releasing unmanaged resources as a last resort, but they're costly: such objects survive an extra collection and run on a special thread at an unpredictable time. In modern code you almost never write one — use IDisposable (next lesson) and SafeHandle instead.

Memory leaks in managed code

The GC can only free unreachable objects. Leaks happen when you keep references you no longer need:

// 1. Static collections that only grow
static readonly List<Order> recentOrders = new();      // never cleared → grows forever
 
// 2. Event subscriptions that are never removed
publisher.Changed += subscriber.OnChanged;             // publisher keeps subscriber alive
 
// 3. Caches without limits or expiry
static readonly Dictionary<string, byte[]> cache = new();

Tools like dotnet-counters, dotnet-gcdump and Visual Studio's memory profiler show which objects are piling up and what keeps them alive.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • The GC traces references from roots; anything unreachable is garbage.
  • Generations make collection cheap: young objects are checked often, old ones rarely.
  • Large objects (≥ 85 KB) go to the LOH, collected with Gen 2.
  • Avoid finalizers and GC.Collect(); leaks come from references you forgot to drop.

Quiz

4 questions · 3 correct to pass
  1. 1.When does the garbage collector consider an object garbage?

  2. 2.Why does .NET use generations?

  3. 3.Can a .NET application leak memory?

  4. 4.Should you call GC.Collect() in normal application code?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)