Reachability & GC roots in Java
Objects alive if reachable from roots; cycles are still collected.
Start from the roots
The GC doesn't ask "does anyone point at you?" It traces from GC roots: local variables in running methods, static fields of loaded classes, live threads, and JNI references. Everything reachable from a root is alive; everything else is garbage.
Cycles don't save you
Java does not use reference counting. After a and b are set to null, no root reaches the pair, so both are collectable, even though they still point at each other.
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null; // the pair is now unreachableWhat happens to the pair?
After that code, what may the GC do with the two Node objects?
Collect bothNothing: they still reference each otherCollect them only after System.gc()
Show the answer
Collect both. The tracing never reaches them, so their mutual references don't matter. Only reference-counting schemes struggle with cycles.
Is a field a root?
An unreachable object has a field pointing at another object. Does that field keep its target alive?
Think about it, then reveal the answer
No. Roots are only the starting points: locals in active frames, statics, live threads, JNI refs. A field inside an unreachable object is never visited, so it keeps nothing alive.
Nulling locals out of habit
Setting locals to null at the end of a method doesn't help: when the method returns, its frame is gone, so those locals stop being roots anyway. Nulling matters for long-lived fields and collections, not for locals.
void handle() {
var data = load();
process(data);
data = null; // pointless: frame ends next
}Statics live long
Static fields are roots that live as long as their class, often the whole app. Everything you add to a static collection stays reachable until you remove it.
static final List<Order> RECENT =
new ArrayList<>();
void place(Order o) {
RECENT.add(o); // never removed...
}Reading a heap dump
Memory keeps growing. A heap dump shows millions of Order objects, and the analyzer's path to GC root runs through static List<Order> RECENT. That chain is your culprit: the static list keeps every order reachable, so the GC isn't allowed to free them.
Key takeaways
- Roots: locals in active frames, static fields, live threads, JNI refs
- Reachable from a root = alive; unreachable = collectable
- Unreachable cycles are collected
- A 'path to GC root' in a heap dump explains a leak
💡 Think of roots as anchors: whatever is chained to an anchor stays; a raft of objects tied only to each other drifts away.
CPython, Python's main implementation, relies mostly on reference counting and needs a separate cycle detector just for the a↔b case. Java's tracing collectors never had that problem.
Practice questions
Which one is NOT a GC root?
- A live thread
- A local variable in a running method
- A static field of a loaded class
- A field of an unreachable object
Check your answer
A field of an unreachable object. Roots are the starting points of tracing. A field inside an unreachable object is never visited, so it keeps nothing alive.
After these lines run, what can the GC do with the two Node objects?
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;- Collect both: no root can reach them
- Collect them only after System.gc() is called
- Nothing: each one still has a reference to it
- Collect only the first node
Check your answer
Collect both: no root can reach them. Once a and b are null, nothing reachable points to the pair. The mutual references don't matter to a tracing collector.