How .NET Garbage Collector works (and when you should care)
In the world of .NET, memory management is an important aspect of any application. Fortunately, you don't have to shoulder this immense task yourself. .NET handles it with the superpower of the Garbage Collector (GC). A GC is an engine that keeps your app fast, responsive, and resource-efficient. Although on a surface level, you don't need to know much about everything going on below your brackets, it is better to understand how memory management works in your application. In this blog, we will discuss the GC and explore ways to better harness its capabilities.

What is a Garbage Collector in .NET?
The GC is a component of the Common Language Runtime (CLR) that performs automatic memory management. The GC allocates memory to a managed program and releases it when it is no longer needed. The automatic management relieves developers from writing code for memory management tasks and ensures unused objects do not consume memory indefinitely.
Any application uses memory for storing data and objects. Operations such as variable declaration, data fetching, file streaming, and buffer initialization are stored in memory. As we know, these operations can be frequent in any application. However, once these objects are no longer in use, we need to reclaim their memory, because any client or server has limited resources. If memory is not freed, all these unused objects accumulate, leading the program to a memory leak. Developers have to handle this situation manually by writing memory-clearing programs. However, this is a tiresome process because applications frequently use variables and other memory objects. A big chunk of the program would be dealing with the deallocation of memory. Besides its time consumption, manual memory release often requires pointer tweaking, and any mishaps in the pointers can crash the application. You may face a double free situation where releasing the same object multiple times can cause undefined behaviour.
This is where the GC comes to the rescue and handles the entire process of allocation and deallocation automatically.
What is the managed heap?
The managed heap is a segment of memory to store and manage objects allocated by the CLR for a process. It's a key part of NET's automatic memory management system, which is handled by the GC. The managed heap is the working ground of a GC.
Phases in Garbage Collection
1. Marking Phase
In the marking phase, the GC scans memory to identify live objects that are still reachable (referenced) from your program's active code. The GC then lists all live objects and marks unreferenced objects as garbage.
2. Relocating Phase
Now the GC updates the references of live objects, so the pointers will remain valid after all these objects are moved in memory later.
3. Compacting Phase
Here, GC reclaims heap memory from dead objects and compacts live objects together to eliminate gaps in the heap. Compaction of live objects reduces fragmentation and enables new memory allocation faster.
Heap generations in Garbage Collection
The GC in .NET follows a generational approach that organizes objects in the managed heap based on their lifespan. The GC uses this division because compacting a portion of the managed heap is faster than compacting the entire heap. Also, most of the garbage consists of short-lived objects such as local variables and lists, so a generational approach is practical too.
Generation 0
Generation 0 contains short-lived objects such as local variables, strings inside loops, etc. Since most objects in the application are short-lived, they are reclaimed for garbage collection in generation 0 and don't survive to the next generation. If the application needs to create new objects while the heap is full, the GC performs a collection to free address space for the new object. In Generation 0, garbage collection frequently reclaims memory from unused objects of the executed methods.
Generation 1
Generation 1 is the buffer zone between Generation 0 and 2. Objects that survived multiple GC cycles are promoted in this generation. Temporary but reused data structures, which are used by multiple methods, have a shorter lifetime than the application itself. The usual timespan of Generation 1 objects spans seconds to minutes. Naturally, these objects do not need to become unnecessary too early like Generation 0 objects. Hence, the GC performs collection less often than Generation 0, maintaining a balance between performance and memory use.
Generation 2
Generation 2 contains long-lived data whose lifespan can be as long as the application lifetime. Survivors of multiple collections end up in Generation 2, such as singleton interfaces, Static collections, application caches, large objects, or Dependency Injection container services.
Unmanaged resources
Most of your application relies on the GC for memory deallocation. However, unmanaged resources require explicit cleanup. The most prominent example of unmanaged resources is an object that wraps an operating system resource, such as a file handle, window handle, or network connection. Objects like FileStream
, StreamReader
, StreamWriter
, SqlConnection
, SqlCommand
, NetworkStream
, and SmtpClient
encapsulate an unmanaged resource, and the GC doesn't have specific knowledge about how to clean up the resource. We have to call them in using a block that calls Dispose()
to release their unmanaged handles properly. You can also write code to place the Dispose()
method when needed.
An example of using a block is below
using (var resource = new FileResource("mytext.txt"))
{
resource.Write("Hello!");
} // Dispose() automatically called here
.NET Garbage Collector best practices to boost app performance
Limit Large Object Heap
In .NET, any object larger than 85000 bytes is allocated on the Large Object Heap (LOH) instead of the normal managed heap. The GC does not compact the LOH because copying large objects imposes a performance penalty. This can lead to fragmentation and wasted space. Their cleanup is expensive since it is performed in Generation 2. Large JSON serialisation results, large collections, data buffers, and image byte arrays are common examples of LOH. Try to limit the usage of such objects, and if it is not practical to fully avoid them, go for reusing them rather than creating them multiple times separately.
For example:
// ❌ BAD: Creates new 1MB buffer each call
void Process()
{
byte[] buffer = new byte[1024 * 1024];
// use buffer
}
// ✅ GOOD: Reuse buffer
static byte[] sharedBuffer = new byte[1024 * 1024];
void Process()
{
// reuse sharedBuffer safely
}
Minimize unnecessary object allocations
Be careful for short-lived objects as well. Although they are collected in Generation 0 but it still increases the collector's workload. Avoid creating variables repeatedly inside frequently called methods. Instead, reuse these objects when possible.
// ❌ Avoid
for (int i = 0; i < 10000; i++)
var sb = new StringBuilder();
// ✅ Better
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
sb.Clear();
Use value types (structs) wisely
For small, short-lived data, opt for value types, as they live on the stack and are auto-cleaned. That way, you can save GC cycles and improve the application's speed. To know more about value types, check out Exploring C# Records and Their Use Cases.
Avoid long-lived object references
Long-lived references are promoted to the Generation 2 heap. That means they occupy memory longer, slowing down GC and increasing overall memory usage. Remove references (set to null
, clear collections) once objects are no longer needed.
As in the code:
// ❌ Bad: Keeping large object references alive unintentionally
static List<byte[]> cache = new List<byte[]>();
void LoadData()
{
cache.Add(new byte[1024 * 1024]); // never cleared
}
// ✅ Better: Clear or dereference when not needed
cache.Clear();
Cache Intelligently
While cache can unload the application and improve performance. But overusing caches can fill the heap with long-lived Generation 2 objects. Only cache data where necessary, and if you use the MemoryCache
, fine-tune expiration/size limits.
Avoid memory leaks (event and static references)
Unsubscribed event handlers or long-lived static lists can keep objects alive in the Generation 2 heap for a long time.
Example:
// ❌ Potential leak
publisher.Event += subscriber.Handler;
// ✅ Unsubscribe when done
publisher.Event -= subscriber.Handler;
Conclusion
The .NET GC is not just a memory sweeper but an unsung hero working round-the-clock to reclaim the heap space and keep your applications alive and efficient. It examines dead objects in memory and releases their memory to keep the resource from being overburdened and fragmentation. In this post, we walked through the different phases of the GC and learned about unmanaged resources. Finally, we went through some tips to make the GC work better. Garbage collection is a huge area that we only scratched the surface of in this post.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website