VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

The Nerdy Magic of .NET Garbage Collection (That You Probably Don't Know)

I've been working on an intro to C# and .NET programming series on YouTube lately. I'm trying to remember what it was like picking up C# and .NET at the very beginning. When you work with a technology for a long time, you just don't realize all the things that you 1) know but don't REALIZE you know, 2) kinda sorta know but they don't really matter so you can let the details slide, and then 3) stuff that you don't know that's really insanely cool. I came across one of those topics that live in 'category 3' -- garbage collection in .NET.

Garbage collection in .NET is one of those things that you don't really ever have to think too hard about (as long as you don't do something super-wrong). But in developing that content, I realized that I didn't really actually have much of a clue about how it actually is implemented.

And lemme just tell ya -- from a nerd perspective, it's really cool.

Before We Go Any Further: Should You Optimize Based on This?
Let me save you some time: this article is meant to be interesting rather than actionable. The .NET garbage collector is genuinely impressive engineering, and I think it's worth understanding how it works. But should you go rewrite your code based on what you learn here?

Probably not.

Here's the deal. If you avoid these core mistakes, you're fine:

  • Don't hold references to objects longer than you need them (especially in static collections or long-lived event handlers)
  • Don't allocate massive objects in tight loops if you can reuse them instead
  • Don't call GC.Collect() manually thinking you're helping

That's basically it. If you're not doing those things, your memory management is almost certainly fine. Oh, and pay attention to using statements and IDisposable -- that's not exactly memory management, but it's related. (That's a topic for another article.)

You could try to optimize your code based on the nerdy details below. But here's the thing: if you don't know that you officially have memory management problems in your app, you probably don't have memory problems in your app. If you have a problem, you'll know -- your app will be slow, or it'll eat all your RAM, or it'll crash. Otherwise, you could make improvements to your code... but it might take from now until the heat death of the universe for those improvements to be measurable, let alone worth the effort.

So read on because this stuff is fascinating, not because you need to rewrite your codebase tomorrow.

"I Know What GC Does" -- But Do You Really?
Most of us know the basics: you create objects, and when you're done with them, the garbage collector eventually cleans them up. You don't have to call free() like in C. Life is good. Sleep easy.

But here's what I didn't know: the how of garbage collection is genuinely clever engineering. It's not just "periodically scan memory and delete stuff." There's an elegant algorithm underneath, and once you understand it, you'll appreciate why .NET performance is as good as it is.

The Mark-and-Sweep Algorithm (Your Objects Are Either Alive or Dead)
At its core, .NET uses what's called a "mark-and-sweep" algorithm. Here's the basic idea:

Phase 1 - Mark: The GC starts from a set of "roots" -- things like static variables, local variables on the stack, and CPU registers. It walks through every object reference it can reach, marking each object as "alive." If an object can't be reached from any root, it's unreachable. It's garbage.

Phase 2 - Sweep: Everything that wasn't marked? Gone. The GC reclaims that memory.

But here's where it gets interesting. The GC doesn't just leave the heap looking like swiss cheese after removing objects. It also compacts the remaining objects, sliding them together to eliminate fragmentation. This is why allocating memory in .NET is crazy fast -- it's basically just incrementing a pointer to the next free spot in a contiguous block of memory.

The Generational Hypothesis (Short-Lived Objects Die Young)
Here's where the real cleverness comes in. The .NET GC is based on something called the "generational hypothesis": most objects die young.

Think about it. In a typical method, you create a bunch of temporary objects -- strings, collections, intermediate calculations. By the time the method returns, those objects are garbage. Meanwhile, your application's configuration, caches, and long-lived services stick around for the entire lifetime of the process.

The GC takes advantage of this pattern by organizing the heap into three generations:

Generation 0 (Gen 0): This is where all new objects are born. Gen 0 is small -- typically around 256KB to a few megabytes. When it fills up, the GC runs. And here's the key: Gen 0 collections only examine Gen 0 objects. Since most objects die young, most Gen 0 collections find a lot of garbage and are extremely fast. We're talking microseconds.

Generation 1 (Gen 1): Objects that survive a Gen 0 collection get promoted to Gen 1. Think of Gen 1 as a buffer -- these objects weren't temporary, but they're not proven to be long-lived either. Gen 1 collections happen less frequently and examine both Gen 0 and Gen 1.

Generation 2 (Gen 2): Survive Gen 1? Welcome to Gen 2. These are your long-lived objects -- singletons, caches, application state. Gen 2 collections are rare and examine the entire heap. They're more expensive, but they don't happen often because long-lived objects, by definition, don't become garbage very often.

The genius of this design is that the GC spends most of its time looking at the smallest portion of the heap (Gen 0) where it finds the most garbage. It rarely bothers examining the large, stable portion of the heap (Gen 2) where objects almost never die.

The Large Object Heap (The VIP Section)
Here's something that surprised me: not all objects follow the generational rules.

Objects larger than 85,000 bytes (about 85KB) go directly to something called the Large Object Heap (LOH). The LOH has different rules. For one, it's not compacted by default. Compacting large objects is expensive -- you're moving megabytes of data around -- so the GC avoids it unless you explicitly ask for it.

This has practical implications. If you're constantly allocating and deallocating large arrays, you can fragment the LOH. The memory is reclaimed, but you end up with gaps that might not fit your next large allocation. This is one of the few ways you can actually run into memory problems in .NET even though the GC is doing its job.

The lesson? If you're working with large objects that have varying sizes, consider object pooling or reusing buffers instead of constantly allocating new ones.

Server GC vs. Workstation GC (One Size Does Not Fit All)
Did you know there are actually two different garbage collectors in .NET? And the one you're using depends on how your application is configured?

Workstation GC is the default for client applications. It's optimized for responsiveness -- it does its work on the thread that triggered the collection and tries to minimize pause times.

Server GC is designed for server applications. It creates a separate heap and GC thread for each processor core. Collections happen in parallel across all cores, which means higher throughput but potentially longer pause times.

You can configure which GC mode you want in your project file:

<PropertyGroup>

    <ServerGarbageCollection>true</ServerGarbageCollection>

</PropertyGroup>

For most ASP.NET Core applications, Server GC is automatically enabled. But if you're building a console app or desktop application that runs on a beefy server, you might want to flip this on manually.

Background GC (Your App Keeps Running)
Here's another thing I didn't fully appreciate: modern .NET uses background garbage collection for Gen 2 collections.

Before background GC, a Gen 2 collection would stop your entire application while it scanned the heap. For applications with large heaps, this could cause noticeable pauses.

With background GC, Gen 2 collections happen on a dedicated background thread while your application continues running. Gen 0 and Gen 1 collections (the frequent, fast ones) can still occur while a background Gen 2 collection is in progress. This dramatically reduces pause times for applications with large heaps.

The Write Barrier (How GC Tracks Cross-Generation References)
Okay, this is where it gets really nerdy. Feel free to skip this if you're satisfied with what you've learned so far. But if you want to understand the implementation at a deeper level, stick with me.

Here's a problem: when the GC does a Gen 0 collection, it only examines Gen 0 objects. But what if a Gen 2 object holds a reference to a Gen 0 object? The GC needs to know about that reference, or it might incorrectly collect a Gen 0 object that's still reachable.

The solution is something called a "write barrier." Every time your code writes a reference into an object (like setting a property to point to another object), the runtime checks if you're creating a cross-generational reference -- specifically, if an older-generation object is now pointing to a younger-generation object.

If so, the runtime records this in a data structure called the "card table." When the GC runs a Gen 0 collection, it checks the card table to find any Gen 2 objects that might be holding references to Gen 0 objects, and it treats those references as additional roots.

The write barrier adds a tiny bit of overhead to every reference assignment, but it enables the generational GC to work correctly and efficiently. It's a beautiful trade-off.

So Why Bother Knowing This?
If I told you at the top not to optimize based on this stuff, why did I just spend 1,500 words explaining it?

Because understanding your tools makes you a better developer, even when you're not actively using that knowledge. When you do hit a memory problem someday -- and eventually you might -- you'll have a mental model for what's happening. You'll know to look for objects that aren't going out of scope when they should. You'll understand why that giant array allocation is causing issues. You'll know what "Gen 2 collection" means in your profiler output.

And honestly? It's just cool. We work with these systems every day, and most of us have no idea what's happening under the hood. Now you do.

Wrapping Up
Garbage collection in .NET is one of those things that "just works" -- until you need to understand why it's working the way it is. The generational design, the mark-and-sweep algorithm, the write barrier, the LOH, background collection -- it's all working together in a carefully orchestrated dance to keep your application running smoothly.

I find it genuinely impressive how much thought went into this system. And now that you know how it works, maybe you'll appreciate it too.

If you want to see me explain this in video form with more examples (and my usual rambling tangents), check out my YouTube video on garbage collection:

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 12/08/2025


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.