You've never called free() in C#. Something else has been doing that job, silently, this entire time.
Every new Customer(), every new List<int>(), every object you've ever created across two entire books of lessons has gone somewhere — and, eventually, has been cleaned up. You never wrote code to release that memory. You never even had to think about it. That's not because memory management is unnecessary in .NET; it's because someone else is doing it for you, continuously, in the background, for the entire life of your program.
That someone is the Garbage Collector (GC) — arguably the single most consequential CLR service you've been relying on, and the one this whole tier has been building toward. It's time to actually understand what it does and how.
In this lesson, you'll learn why automatic memory management exists, what "garbage" actually means to the runtime, and how the GC decides what's safe to reclaim and when.
The Garbage Collector is a part of the CLR that periodically looks at everything currently on the managed heap, figures out which objects your program can no longer possibly reach, and frees the memory those unreachable objects were using — automatically, without you writing any cleanup code.
The GC is a tracing, reachability-based automatic memory manager. It does not track "how many things point at this object" (that's a different technique called reference counting, used by some other languages). Instead, periodically, it starts from a known set of roots — local variables currently on the stack, static fields, CPU registers holding object references — and walks the graph of everything reachable from those roots. Anything not reachable this way is, by definition, garbage: your program has no possible way to ever use it again, so it's safe to reclaim.
malloc/free (or new/delete) explicitlyIn languages without a GC, the programmer is personally responsible for deciding exactly when a piece of allocated memory is no longer needed and calling a function to release it. This sounds simple in principle, but at real-world scale it's one of the largest sources of serious bugs in software history:
free() on it. The memory is gone for the life of the process. A long-running server leaking memory on every request will eventually run out of memory and crash.These bugs are notoriously hard to find because the symptom (a crash, or corrupted data) often shows up long after and far away from the actual mistake (the missing or extra free() call).
The GC removes the decision of "when to free this" from the programmer entirely. You simply stop using an object — by letting the last reference to it go out of scope, or setting it to null, or letting a containing object become unreachable — and the GC figures out, safely and automatically, when it's truly gone for good. Because it only ever reclaims objects that are provably unreachable from any root, use-after-free and double-free bugs on managed memory are structurally eliminated, not just made less likely. Memory leaks are still possible in .NET (if something keeps an unwanted reference alive), but the entire class of "freed too early" or "freed twice" bugs simply cannot happen through ordinary managed code.
void ProcessOrder()
{
var order = new Order(); // 'order' (a local var) is a root — Order object is reachable
order.Items.Add(new OrderItem("Widget", 3)); // reachable via order.Items
}
While ProcessOrder is executing, order is a local variable on the stack — a GC root. The Order object it points to is reachable, and so is the OrderItem reachable through order.Items.
Once ProcessOrder returns, the local variable order ceases to exist — it's no longer a root. If nothing else in the program still holds a reference to that Order object (say, it wasn't saved anywhere, and no event handler captured it), the object — and everything reachable only through it — is now unreachable.
The GC does not run on a fixed timer, and it does not run the instant an object becomes unreachable. It runs when the runtime decides memory pressure warrants it — most commonly, when a generation of the heap (covered in the next lesson) has accumulated enough new allocations to be worth examining. This is a deliberately non-deterministic point in time from your code's perspective: you cannot reliably predict exactly when a given unreachable object's memory will actually be reclaimed.
When a collection runs, the GC (conceptually) walks the object graph starting at every current root, marking every object it finds along the way as "still alive." This is the mark phase of a mark-and-sweep-style approach: mark what's reachable, then treat everything unmarked as reclaimable.
Objects with no path back to any root are garbage — their memory is freed. .NET's GC typically also compacts the surviving objects (moves them together, eliminating gaps left by the reclaimed ones), which keeps future allocation fast and avoids fragmentation. You'll see exactly why compaction matters for allocation speed in the memory allocation lesson later in this module.
public class Report
{
public string Title { get; set; } = "";
}
Report CreateReport()
{
var report = new Report { Title = "Q3 Summary" }; // allocated on the heap
return report;
}
void RunReport()
{
var r = CreateReport(); // r now holds the only reference
Console.WriteLine(r.Title);
// r goes out of scope here when RunReport returns
// From this point on, the Report object is unreachable
// — eventually, a GC will reclaim it. Exactly when is not
// something your code can rely on or observe directly.
}
Code → Meaning → Result:
CreateReport, the new Report is reachable through the local variable report, and then through the returned reference stored in r.RunReport returns, r no longer exists as a root, and (assuming nothing else references it) the Report object becomes garbage.Consider an ASP.NET Core API handling thousands of requests per minute. Every request typically allocates a burst of short-lived objects: a request DTO, a few EF Core entity instances, a result object, maybe some LINQ intermediate collections. Once the response is written and the request completes, every one of those objects instantly becomes unreachable — nothing in the app still references them.
Over the life of the process, this produces an enormous, continuous churn of allocate-then-discard. The GC is what makes this workable at all: without it, the server would need to explicitly track and free every one of those thousands of small, per-request objects, correctly, every single time, with zero mistakes — an essentially impossible bar to hold in a codebase with any real complexity. The GC instead lets developers focus entirely on business logic, while it continuously reclaims the constant stream of request-scoped garbage in the background.
This also explains why .NET provides concurrent (background) GC as the default mode for server workloads: a request-serving process cannot tolerate the entire application freezing for a long pause every time memory needs reclaiming, so much of the GC's work happens on a background thread, concurrently with your application continuing to run.
Manual memory management is like living in a house where you personally have to carry every piece of trash out to the curb the moment you're done with it — forget one item, and it piles up forever (a leak); throw something away that a roommate is still using, and chaos follows (use-after-free); try to throw the same bag out twice and you'll trip over your own confusion (double-free).
The GC is a cleaning crew that periodically walks through the house, checks what's genuinely still in use — is anyone sitting on this chair, is this book on anyone's active reading list — and only removes what's truly abandoned. You never have to remember to take anything out yourself. The trade-off: the crew needs to periodically walk through and check, and while they're actively working in a room, that can briefly get in the way of what you're doing there — which is exactly why modern GC designs try hard to do most of that checking without making you stop and wait.
It doesn't. Going out of scope (or setting a reference to null) makes an object eligible for collection — it removes the object from the reachable set. But the actual reclamation only happens the next time a collection runs against the generation that object lives in, which could be milliseconds later or, if memory pressure is low, considerably longer. Reachability and reclamation are two separate events.
No — .NET's GC is a tracing collector, not a reference-counting one. Reference counting (incrementing/decrementing a counter on every assignment) has its own well-known weakness: two objects that reference each other, but that nothing else references, would keep each other's count above zero forever under naive reference counting — a reference cycle that leaks. A tracing collector like .NET's has no such problem: if neither object is reachable from an actual root, both are correctly identified as garbage, cycle or not.
The GC eliminates use-after-free and double-free bugs, but it cannot eliminate leaks caused by your own code accidentally keeping something reachable forever — a static collection you keep adding to and never clear, an event subscription that's never unsubscribed, a cache with no eviction policy. If a root still points to it, directly or transitively, the GC is doing exactly its job by keeping it alive. "Managed memory leak" is a real, common category of production bug — it just has a different root cause than in unmanaged languages.
GC.Collect() manually "to be safe" Sprinkling GC.Collect() throughout application code on the assumption it will improve performance or memory usage.
In almost every real scenario, this makes things worse: it forces a full, synchronous collection at a moment the runtime's own heuristics would not have chosen, interrupting work that was proceeding fine. Trust the GC's own scheduling; it has far more information about actual memory pressure than a single call site does.
null Writing myObject = null; and treating that as equivalent to memory being immediately freed.
Setting a reference to null removes one path to the object — it makes the object unreachable if that was the only path. The memory itself is reclaimed later, whenever a collection actually runs. For deterministic, immediate resource cleanup (file handles, network sockets), C#'s IDisposable/using pattern — which you already learned in earlier tiers — is the correct tool, not relying on GC timing.
Assuming rising memory usage means the garbage collector "isn't working" or is somehow broken.
Almost always, growing managed memory means something is still reachable that you didn't intend to keep alive — a forgotten event subscription, an ever-growing static cache, a captured closure holding a reference longer than expected. The fix is finding and breaking that reference chain, not distrusting the GC itself.
You can't turn the GC off for ordinary managed objects, and you almost never should try to. What this lesson changes is how you reason about memory-related symptoms:
IDisposable exists alongside the GC: the GC handles memory, but it has no idea a file handle or network connection needs to be released promptly — that's still your explicit responsibility.IDisposable/using for unmanaged or scarce resources (files, sockets, database connections), never for ordinary heap-allocated objects.
You've learned what the GC is, why it exists, and how it decides what's garbage. Let's test the core mental model.
1. What does it mean for an object to be a "GC root"?
Correct: B
Why B is correct: Roots are the known, trusted starting points the GC uses to begin its trace — local variables on the stack, static fields, and CPU registers holding references — not heap objects themselves. Anything reachable by following references starting from a root is considered live.
Why A is incorrect: Roots aren't defined by allocation order — a variable declared near the very end of a method's execution is just as much a root as one declared earlier.
Why C is incorrect: Survival count relates to generations, a topic in the next lesson — it's unrelated to what makes something a root.
Why D is incorrect: Object size relates to the Large Object Heap, a separate concept covered in the next lesson — it has nothing to do with being a root.
Reinforcement: Roots are the "known safe" starting points; everything else's status (reachable or garbage) is determined by whether a chain of references connects it back to one.
2. A local variable holding a reference to an object goes out of scope when its containing method returns, and nothing else in the program references that object. What happens immediately at that moment?
Correct: B
Why B is correct: Going out of scope removes a root, which can make the object unreachable — but reclaiming the memory is a separate, later event that only happens when a collection actually runs and traces the heap.
Why A is incorrect: There's no synchronous, instant reclamation tied to scope exit — this is precisely the common misconception the lesson called out directly.
Why C is incorrect: Collections happen throughout a process's lifetime, triggered by allocation pressure — not only at process exit.
Why D is incorrect: There's no "ownerless object" exception in the CLR's memory model — unreachable objects simply become collectible, silently, with no exception involved.
Reinforcement: Eligibility (a state) and reclamation (an event) are distinct — this is the single most important timing distinction in this lesson.
3. Object A holds a reference to Object B, and Object B holds a reference back to Object A — but nothing else in the running program references either of them. What does .NET's tracing garbage collector conclude?
Correct: B
Why B is correct: Because .NET's GC is a tracing collector, not a reference-counting one, it doesn't care how many references point at an object — only whether a real root can reach it. Since neither A nor B is reachable from any root, the trace never marks either as live, and both are correctly reclaimed together.
Why A is incorrect: This describes a real weakness of naive reference-counting garbage collectors — but that's explicitly not the algorithm .NET's GC uses, which is why this scenario isn't a problem for it.
Why C is incorrect: Reference cycles are an entirely normal, expected pattern (e.g., parent-child relationships) that tracing GCs handle correctly with no special exception or error.
Why D is incorrect: Reachability, not creation order, determines collectability — creation order plays no role in this decision at all.
Reinforcement: This is exactly why the lesson emphasized "tracing, not reference-counting" — cyclic references are one of the classic problem cases reference counting handles poorly, and one that tracing collectors like .NET's handle correctly by design.
4. A developer notices their long-running service's memory usage keeps climbing steadily over several days and suspects "the garbage collector isn't working." What is the most likely actual explanation, based on this lesson?
Correct: B
Why B is correct: The GC only reclaims what's unreachable — if something in the application logic (a growing cache with no eviction, a forgotten event subscription) keeps objects rooted, the GC is behaving exactly correctly by refusing to collect them. This is the "managed memory leak" pattern described in Common Confusion.
Why A is incorrect: The GC is a mature, extensively used component of the runtime — steadily growing memory in a real application is overwhelmingly a rooted-reference issue in application code, not a runtime defect.
Why C is incorrect: There's no inherent time limit on a .NET process's healthy lifetime — well-written services run for extended periods without memory issues; steady growth points to a specific, findable rooting bug, not an inherent platform limitation.
Why D is incorrect: The GC runs continuously throughout a process's life, triggered by allocation pressure as needed — it is not a one-time startup operation.
Reinforcement: The right diagnostic question for growing memory is always "what's still referencing this?" — a memory profiler that shows retained object graphs and their root paths is the standard tool for answering it.
5. Why does concurrent (background) GC exist as the default for typical server workloads in modern .NET?
Correct: B
Why B is correct: Concurrent GC allows a substantial portion of the collector's work to happen on a background thread while the application keeps running, shrinking the duration of the unavoidable stop-the-world portions — this is exactly why it matters for latency-sensitive, request-serving workloads.
Why A is incorrect: The lesson was explicit that pauses are reduced, not eliminated — some brief stop-the-world work remains necessary even with concurrent collection.
Why C is incorrect: The GC continues to run during active request handling — concurrent GC is about overlapping its work with the application, not pausing collection while requests are served.
Why D is incorrect: .NET's GC remains a tracing collector throughout — concurrent GC changes when and how tracing work is scheduled relative to application threads, not the fundamental algorithm used to determine reachability.
Reinforcement: "Reduce pauses" is the accurate, defensible claim about concurrent GC — treating it as "pauses are gone" leads to false confidence when diagnosing real latency spikes in production.
You now understand why the GC exists and how it decides what's safe to reclaim — next, you'll see how .NET makes this fast in practice with generations.
dotnetmadeeasy.com — Learn C# and .NET, the right way.