Garbage Collection in C#: How .NET Cleans Up After You ๐งน
If youโve ever written C#, youโve already used one of its most powerful features โ whether you realized it or not. Every time you new up an object, C# quietly handles the messy job of memory management for you.
You donโt have to free() anything, you donโt have to track pointers, and you never have to worry about segmentation faults.
Thatโs thanks to the .NET Garbage Collector (GC) โ one of the unsung heroes of the runtime.
But while C# developers get to ignore memory most of the time, understanding how the garbage collector works is one of those โgrown-upโ moments in your coding journey. Itโs where you stop writing code that runs, and start writing code that runs efficiently.
๐ง What Garbage Collection Actually Is
When you create an object in C# like this:
Using memory from the managed heap
That object gets allocated in memory โ specifically on the managed heap, a section of memory controlled by the CLR (Common Language Runtime).
Every allocation adds data to this heap, and the runtime keeps track of whatโs still being used and whatโs not.
Once something no longer has any references pointing to it โ meaning nothing in your code can reach it โ it becomes eligible for collection.
Then, the garbage collector eventually swoops in, reclaims that memory, and makes it available for new allocations.
๐๏ธ In short:
You create objects.
The CLR tracks references to them.
When there are no references left, GC frees the memory.
You get automatic memory management with minimal effort.
Itโs a simple idea โ but the way the .NET GC pulls it off is surprisingly sophisticated.
โ๏ธ How the .NET Garbage Collector Works
The .NET GC isnโt just one big cleanup thread. Itโs a generational, compacting, mark-and-sweep garbage collector.
Letโs unpack that โ because each of those terms matters.
1. Generational Collection
Not all objects live the same amount of time.
Some are short-lived โ like strings built inside a loop (local scope).
Others are long-lived โ like singletons, caches, or static configuration.
To optimize for this, the .NET GC organizes memory into three generations:
The 3 generations of garbage collection
When a garbage collection occurs, it usually only checks Gen 0.
That means the GC can clean up frequently and quickly, without scanning the entire heap every time.
Objects that survive a cleanup get promoted to the next generation, where theyโre checked less often.
โ
Why this matters:
This tiered system makes GC extremely efficient โ short-lived junk gets cleaned fast, while long-term data doesnโt keep slowing things down.
2. Mark and Sweep
The GC doesnโt just delete random objects โ it has to know whatโs still in use.
Hereโs how it figures that out:
Mark Phase:
The โmarkโ phase begins by identifying a set of objects called root references โ these are the starting points that are always considered alive. Root references include:Local variables currently in use by methods on the stack.
Static fields on classes.
CPU registers that hold object references.
Objects pinned for interop (e.g., when passing managed data to unmanaged code).
From these roots, the GC performs a kind of graph traversal through all referenced objects โ following every field, property, or array element that points to another object. An object is considered reachable if it can be traced, directly or indirectly, from one of these root references. If thereโs a path from a root to the object, itโs alive. If no path exists โ meaning no variable, field, or structure anywhere in your program can reach it โ the GC marks it as unreachable and prepares to collect it in the next sweep.
You can picture it like shining a flashlight from your programโs โentry points.โ Every object the light touches is kept; everything left in the dark is garbage.
Sweep Phase:
Anything not marked gets flagged as garbage and reclaimed.
Thatโs why if you lose all references to an object (by setting them to null, or letting them fall out of scope), the GC can safely free that memory.
3. Compaction
After garbage is swept away, the memory left behind has gaps. To avoid fragmentation, the GC compacts the heap โ moving live objects together and updating references. This makes future allocations faster because the heap stays contiguous.
Think of it like cleaning a messy drawer: you throw out the junk, then push the remaining stuff to one side so thereโs space for new items.
Why C# Uses a Managed Heap
In lower-level languages like C or C++, developers have to manually allocate and free memory. Thatโs flexible but error-prone. C# and .NET take the opposite approach: you trade a little control for a lot of safety.
The managed heap lets the runtime:
Automatically detect when memory is safe to free.
Prevent use-after-free and dangling pointer bugs.
Optimize allocation speed (heap allocations are extremely fast in .NET).
Compact memory automatically to avoid fragmentation.
You can think of it as automatic memory insurance โ you pay a tiny performance premium, but you never lose your data to a bad pointer dereference.
When Garbage Collection Happens
Garbage collection doesnโt happen at random โ the CLR triggers it based on memory pressure and allocation patterns.
Here are the main triggers:
Gen 0 fills up โ most common.
The system runs low on memory.
You call
GC.Collect()manually (not recommended).The app goes idle โ background GC runs opportunistically.
Normally, you should never call GC.Collect() yourself โ the runtime is almost always better at timing collections than you are.
The Managed Heap in Action
Letโs visualize it:
You allocate three objects. They go into Gen 0.
The GC runs and finds that two are no longer referenced.
Those get deleted, and the surviving one gets promoted to Gen 1.
You allocate more objects; eventually, Gen 0 fills up again.
GC runs again, and the cycle continues.
In a large application, most collections happen in Gen 0 and complete in milliseconds โ you usually donโt even notice.
Server vs. Workstation GC
The .NET runtime provides two GC modes, tuned for different environments:
Two different kinds of garbage collection, tailored to their desired performance
You can control this via your projectโs .config file or runtime settings.
If youโre building Unity games or desktop tools, youโre on Workstation GC. If youโre running ASP.NET or backend services, youโre using Server GC for performance.
Large Object Heap (LOH)
Objects larger than 85 KB donโt go into the regular heap โ they go into a special area called the Large Object Heap (LOH).
Why? Because moving big blocks of memory during compaction is expensive.
The LOH skips compaction to avoid that cost โ but that also means it can fragment over time.
If your app allocates and frees lots of large objects (like big byte arrays or image buffers), you may eventually hit performance hiccups due to fragmentation.
Memory Leaks in a Managed Language?
Wait โ if C# manages memory for you, how can you still leak memory? Itโs not about freeing; itโs about references.
If you accidentally keep references to objects you no longer need, the GC sees them as โin useโ and wonโt collect them.
Common causes:
Static fields holding onto objects.
Events not unsubscribed (
+=without-=).Caches or lists that never clear.
Long-lived objects referencing short-lived ones.
Garbage collection doesnโt save you from poor reference management โ it just automates when memory is freed.
โ ๏ธ Common GC Misconceptions
โ โGarbage collection slows down my program.โ
Not necessarily. Modern GC is extremely efficient โ and it often improves performance by compacting memory and preventing fragmentation.
โ โGC runs constantly.โ
Nope. It runs only when needed โ typically when Gen 0 fills up.
โ โYou should always call GC.Collect() manually.โ
Almost never. The runtime has decades of tuning behind its heuristics โ trust it unless you have a very specific edge case.
How to Work With the Garbage Collector
Here are a few practical tips that make a big difference:
โ 1. Minimize Unnecessary Allocations
Avoid creating objects in tight loops or per-frame in Unity. Instead, reuse objects (object pooling) or use structs for small, short-lived data.
โ
2. Use using and IDisposable
Garbage collection frees memory, but not unmanaged resources (like file handles, sockets, or GPU memory). The using keyword ensures those get cleaned up properly:
Not all resources are managed for you, so be careful
โ 3. Be Mindful of Events
Unsubscribed event handlers are one of the top causes of memory leaks in .NET. Always pair += with -= when appropriate.
โ 4. Avoid Holding Unnecessary References
Set large collections to null when done, or use weak references for caches.
โ 5. Use Value Types When Possible
Small structs stored on the stack donโt need GC cleanup at all โ theyโre removed automatically when they go out of scope.
Advanced Tip: Weak References
If you want to reference an object without preventing it from being collected, use a WeakReference<T>.
Example:
The weak reference wonโt save it from the garbage collector
This allows the GC to reclaim that object if memory is tight โ perfect for image or asset caches.
Observing the GC in Action
You can monitor garbage collection using the Performance Profiler in Visual Studio, dotMemory, or Unityโs Profiler window.
Watch for:
GC Allocations per frame
Collection frequency
Heap size growth over time
If you see frequent full (Gen 2) collections or excessive LOH growth, youโre allocating too aggressively.
The Big Picture
C#โs garbage collector is one of the reasons .NET apps are so stable. It handles millions of allocations per second, keeps your heap compact, and does all of it in the background.
But while itโs automatic, itโs not invisible. When you understand how it works, you stop fighting the runtime and start writing code that flows with it.
๐ง TL;DR
Garbage collection automatically reclaims unused memory in .NET.
It works in generations for speed and efficiency.
Large objects go to the LOH, which can fragment.
Memory leaks still happen if you hold onto references.
Donโt call
GC.Collect()โ let the runtime handle it.Profile allocations, use structs smartly, and dispose of unmanaged resources.
Garbage collection isnโt just about freeing memory โ itโs about keeping your programs fast, safe, and clean without breaking your flow as a developer.
So next time you hit โRun,โ take a moment to appreciate the silent janitor cleaning up behind your code.