⚙️ JVM Internals & Memory · Advanced

Reachability & GC roots in Java

Objects alive if reachable from roots; cycles are still collected.

🧩 The mysteryTwo objects point at each other, so each has a reference keeping it alive. Forever, right? Java's garbage collector disagrees. Here's how it decides who lives.

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 unreachable
🔮 Predict it

What happens to the pair?

After that code, what may the GC do with the two Node objects?

  1. Collect both
  2. Nothing: they still reference each other
  3. Collect 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.

🤔 Think first

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.

⚠️ The trap

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...
}
💼 In the real world

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

  1. Roots: locals in active frames, static fields, live threads, JNI refs
  2. Reachable from a root = alive; unreachable = collectable
  3. Unreachable cycles are collected
  4. 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.

🤯 Did you know?

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?

  1. A live thread
  2. A local variable in a running method
  3. A static field of a loaded class
  4. 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;
  1. Collect both: no root can reach them
  2. Collect them only after System.gc() is called
  3. Nothing: each one still has a reference to it
  4. 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.

Tracing the whole heap every time would be slow. Next: the bet that makes GC fast. Most objects die young.