Learning outcome
By the end of this lesson, you should be able to explain three interview staples clearly: what a GC root is, what reachability means, and why an object stays alive even when it looks “unused.” The core idea is simple: .NET GC does not collect objects because your code stopped mentioning them; it collects objects only when they are unreachable from any GC root. That distinction is the difference between “I can’t see it anymore” and “the runtime can prove nobody can reach it anymore.”
Intuition
Imagine a city of islands connected by bridges. A GC root is a mainland entrance: a place the runtime always starts from when checking what is still alive. If there is any bridge path from the mainland to an island, that island is considered reachable and must stay. If no root can reach an island, the GC may reclaim it later. The important nuance is that any rooted path is enough to keep an object alive, even if your current method no longer needs it.
A common interview trap is confusing “local variable went out of sight” with “object is collectible now.” JIT-reported liveness can end before a local variable leaves lexical scope, or last longer under debugging and other execution conditions. A source line or closing brace is not a collection-time guarantee. So the practical answer is not “after the last use,” but “after there is no rooted reference chain to the object.”
Deep dive
GC roots are the starting set for reachability analysis. Typical roots include:
- Thread stack references in active methods
- Static fields
- CPU registers and JIT-tracked locals
- GC handles
- Finalizer queue / resurrection-related runtime structures
References held by reachable objects extend the reachable graph; those ordinary references are not additional GC roots.
The GC begins at these roots, walks object references, and marks everything it can reach. Anything not marked is eligible for collection. This is why an object can survive even when it seems “orphaned” in your business logic: some root still points to it indirectly.
A useful mental model is: the GC does not ask “has this object been used recently?” It asks “can I prove no live path exists to this object from any root?” If the answer is no proof, the object stays.
This also explains why caches, events, static lists, and long-lived singletons are frequent sources of memory retention. A cache entry that is still referenced by a static dictionary remains reachable. A subscriber held by an event on a rooted publisher may stay alive because the event invocation list keeps the target reachable. A captured local in a closure can stay alive because the generated closure object is rooted by a delegate stored somewhere longer-lived.
Failure modes
The most common mistakes are:
- Assuming “not used anymore” means collectible. Source-code visibility is not the GC rule.
- Forgetting about indirect references. A rooted object can keep an entire graph alive.
- Leaking through events. If a long-lived publisher holds a delegate to a short-lived subscriber, the subscriber may remain reachable.
- Leaking through static state. A non-null static field can retain its target until the reference is cleared or its owning context is unloaded; static caches need an explicit eviction policy.
- Confusing pinning with reachability. Pinning prevents movement. A live pinned GC handle also retains its target until the handle is freed; retaining a raw unmanaged pointer alone does not keep an object alive.
For interview answers, it helps to say: “An object becomes eligible for collection when it is unreachable from all GC roots. As long as a rooted chain exists, it stays alive.” That is the entire rule in one sentence.
Interview drill
Try answering these out loud:
- What is a GC root?
- Why can an object survive even if my method no longer references it?
- How can an event handler cause memory retention?
- Why might a static cache keep objects alive for the lifetime of the app?
- What is the difference between pinned and reachable?
A strong answer mentions that collection is based on reachability, not on “last use,” and that the GC traverses object graphs from roots.
Revision checklist
- GC roots are the runtime starting points for reachability analysis.
- Reachable objects stay alive; unreachable objects may be collected.
- A rooted reference chain anywhere in the graph keeps the target alive.
- Static fields, stack locals, registers, handles, and event subscriptions can all act as retention paths.
- “Not referenced in my code anymore” is not the same as “unreachable.”
Worked retention scenario
Consider an interview-service process with this strong-reference chain:
static cache -> subscriber -> request context -> large payload
The payload can remain reachable even after request handling finishes, because the static cache still retains the subscriber. Removing a local variable does not remove that path. An ownership-aware cleanup routine can unsubscribe the subscriber or evict the cache entry when the subscription is no longer needed.
After the path is removed, inspect other retaining paths before calling the problem solved. Other caches, delegates or temporary runtime roots may still retain the object. Making an object eligible for collection does not specify when memory will be reclaimed. A weak-reference probe, sleep or forced collection is not a reliable collection-time promise.
In an interview, separate three questions: who owns the subscription, what strong path retains the payload, and when the owner should remove that path. Use a memory profiler's retaining-path view when diagnosing the live application; do not add forced collections as a substitute for fixing ownership.