GC Roots, Reachability, and Why Objects Stay Alive in .NET
Trace GC roots and the references keeping objects alive.
The request ended, yet a static cache still holds its subscriber. What keeps the payload alive? Follow the complete reference chain in the retention scenario. Removing a local or unsubscribing a different event does not remove a cache entry.
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. For this exact chain, evicting the cache entry or clearing the subscriber’s retained request context can break the shown path. Unsubscribing an event would remove an event-delegate path, if one exists; it does not remove the separate static-cache reference shown here. Choose cleanup from the actual retaining graph.
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.
Check one decision
The only shown retaining chain is static cache → subscriber → request context → payload. The subscriber unsubscribes from a separate event but remains in the cache. What follows?
Show answer
Correct answer: D
Event unsubscription removes that event’s delegate edge. It does not remove an unrelated dictionary entry or the references held by that entry. Even after removing the shown path, check for other roots.
Hint: Name the particular edge changed by each cleanup action.
Trace a path from a root
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.”
Roots and ordinary object references
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.
Answer key
- A root is a runtime starting point for tracing managed references.
- Another rooted path may still reach it; your method's local is only one possible path.
- A rooted publisher can retain the subscriber through its delegate. Unsubscribe when ownership ends.
- A static cache retains entries until eviction, reference removal, or unloading removes that path.
- Reachability keeps an object live. Pinning constrains movement; a live pinned handle also retains its target.
TechInterviewNotes Editorial Team · Updated 28 Oct 2026 ·