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:
- It starts from the roots: static fields, local variables and parameters of methods currently running on any thread, and a few internal handles.
- It follows every reference from those roots, marking each object it reaches as alive.
- 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).
| Generation | Contains | Collected |
|---|---|---|
| Gen 0 | Newly allocated objects | Very often, very fast |
| Gen 1 | Objects that survived one collection — a buffer | Less often |
| Gen 2 | Long-lived objects | Rarely (a "full" collection) |
| LOH | Large objects (≥ 85,000 bytes), logically part of Gen 2 | With Gen 2 |
Objects that survive a collection are promoted to the next generation. Try it:
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.
- 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
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 pass1.When does the garbage collector consider an object garbage?
2.Why does .NET use generations?
3.Can a .NET application leak memory?
4.Should you call GC.Collect() in normal application code?
To complete this lesson
- ○ Pass the quiz (3/4 correct)