A small dive into how the .NET Garbage Collector works
I always heard that we should care about memory management since my early days as a developer. Of course, at that time, as I was learning how to be a developer on Windows using Delphi, the recommendation was basically to “always remove the values of your variables” or “set the variables to null”. I didn’t fully understand it, but I was replicating it. As time passed, I delved into .NET and learned that we don’t need to do that anymore because of the Garbage Collector.
What is it?
The Garbage Collector (GC) is basically a memory manager that will handle the allocation and release of memory for a .NET application. In case you are wondering, that’s pretty much one of the things that makes C# a managed programming language. Its use should help any developer to manage the memory usage of an application without the need to write special code for it, which reduces memory leaks.
How does it work?
The GC works in the heap memory, which can be compared with a set of unorganized boxes in a room. And, in order to find anything in that room, you need to have a paper telling what the color of that box (the reference) is.
The GC works in steps: Marking, relocating, and compacting.
- Marking: A list of all “live” objects is created by following every reference, trying to find every reachable object.
- Relocating: All references to the objects that will be moved during the next phase are updated.
- Compacting: The “live” objects will be moved to the beginning of the heap, releasing the “dead” objects
If we think of the boxes and papers, it would be the same if someone were to go into the room and do a quick check on the boxes to identify the ones needed and move the others around.
The generational model
Performance-wise, .NET divides the managed heap into generations, where the younger objects “die” first. Here’s how it looks:
- Generation 0: Youngest generation, containing short-lived objects, where the CG visits more often.
- Generation 1: Middle generation, acting as a buffer between short-lived and long-lived objects.
- Generation 2: Oldest generation, where GC will find the long-lived objects. The collections here happen less often and are more expensive.
Once an object is created, it will automatically start in Generation 0. Consequently, that object will be cleaned up once it's no longer in use. But in case the GC identifies that the object is still in use during its execution, it will be promoted to Generation 1. The same thing happens to the objects still in use in Generation 1 (promotion to Generation 2). So, you can imagine that local variables are mostly going to die in Generation 0, while static data and objects that stay alive for the duration of a longer process will most likely end in Generation 2.
How often is the GC executed?
The GC doesn’t have a running schedule (example: every 5 minutes), but instead, it triggers when certain memory or performance conditions are met. Let’s look at some conditions.
- Triggers
-
- Allocation: Each GC generation has an amount of allocated memory, which means that when a generation becomes full, the GC triggers a collection to clear space for new objects.
- Physical memory: If the OS has a low amount of physical memory available, the GC will execute to free up as much as possible
- Application shutdown: When the application domain is shut down, the GC will clear all objects associated with it
- Manual call: A developer can explicitly call GC.Collect(), but this is not recommended, as it can interfere with the GC's self-tuning logic.
- Generation frequency
As the GC works in generations (Generation Model), then we have different parts of the memory heap being cleaned at different intervals:
-
- Gen 0: This is the most frequent, running very often. As it assumes that most objects in this pool are temporary (i.e.: local variables), it’s designed to be extremely fast.
- Gen 1: It runs less frequently and only triggers when it reaches its own threshold or when a Gen 0 collection didn’t free up enough space.
- Gen 2: This is the most expensive collection as it’s running a full GC, cleaning the entire memory heap. As this can cause a noticeable pause in the application, the GC will avoid running unless it’s absolutely necessary.
Conclusion
The .NET Garbage Collector (GC) frees developers from the burden of manual memory management, letting them focus on building features instead of tracking objects. The steps (mark, relocate, and compact) associated with the generational model make the GC clean up objects efficiently while keeping the application performance smooth.
Ultimately, the GC is one of the reasons .NET is a managed, reliable, and productive platform, reducing memory leaks and improving stability without the complexity of manual intervention.