skip to content

Reachability & GC Roots

Liveness is decided by tracing from GC roots — stack locals, static fields, live threads, native references, held monitors — not by counting references. Interviewers ask "how does the JVM know an object is garbage?" to see whether you reach for tracing and can explain why reference counting cannot reclaim cycles.

on this pageshow

questions

3

Name the categories of GC roots a JVM collector enumerates before tracing, and give an example of an object kept alive by each.

level: middleimportance: must knowfreq 52%

answer

  1. stack locals + operand slots
  2. static fields (and their class loader)
  3. JNI local and global handles
  4. live Thread objects, held monitors
  5. VM internals: loaders, interned constants, JVMTI

basics

~20 s

Local variables and operand-stack slots in active frames of every thread; static fields of loaded classes; JNI local and global references; live Thread objects themselves; objects currently used as monitors; plus JVM-internal references such as class loaders and resolved constant-pool entries.

solid answer

~60 s

A GC root is any reference the collector can obtain without first traversing the heap, because it comes from execution state or VM structures. The standard categories: - **Thread stacks** — locals and operand-stack entries in every frame of every live thread; the largest and most dynamic root source. - **Static fields** — class variables in the class's storage; alive as long as the class and its loader are alive. - **JNI references** — locals held during a native call and globals created explicitly by native code, which survive until deleted. - **Live threads** — the `Thread` objects themselves, which is why a running thread and everything it holds cannot be collected. - **Monitors** — objects currently locked or waited on, so a synchronized-on object cannot vanish mid-critical-section. - **VM-internal roots** — the class loader hierarchy, resolved constant-pool entries including interned strings and class literals, exceptions in flight, and JVMTI debugger or profiler references. An important corollary: because static fields are roots and are anchored by their class loader, retaining one class loader retains every class it defined and every object those classes' statics reference.

go deeper

for a junior

List the main categories with one example each: stack locals, static fields, live threads.

for a middle

Explain why each category exists — references the collector can find without reading the heap graph — and include JNI references and monitors.

for a senior

Reason from root labels in a heap analysis: static field means a class-level registry, Thread means an executor or ThreadLocal, JNI global means native code; and explain the class-loader retention chain.

for a principal

Use the root taxonomy to set architectural rules: which components may own process-lifetime statics, how ThreadLocal use is bounded on pooled threads, and how native integrations must manage global references.

## Why a root set exists at all Tracing needs somewhere to start. The starting set must be findable without reading the heap graph, otherwise the definition would be circular. So roots are drawn from the places the running machine keeps references outside the object graph: thread execution state, class-level storage, native-code handles, and VM bookkeeping. ## The categories in detail **Thread stacks and registers.** Every live thread has frames, and each frame has a local variable array and an operand stack. Any slot holding an object reference is a root. In compiled code, references can also live in machine registers, and those count too. This root source changes constantly, which is why it is read while threads are stopped at a well-defined point. **Static fields.** A static field is storage attached to a class rather than an instance. It lives as long as the class does, and classes loaded by the bootstrap or platform loaders effectively live for the process lifetime. This is the number-one accidental retainer in application code: a static `Map`, a static list of listeners, a static singleton with a growing buffer. **JNI local and global references.** Native methods manipulate Java objects through handles. JNI locals are valid for the duration of the native call and released automatically; JNI globals persist until `DeleteGlobalRef`. Both are roots, because the collector cannot read native stack slots the way it reads Java frames. A native library that creates globals and forgets to delete them produces retention invisible to pure-Java reasoning; heap tools label such paths with a JNI global root. **Live `Thread` objects.** A started, unfinished thread is itself a root. That is why a task submitted to a never-terminating thread, or an object referenced from a thread's field, cannot be collected while that thread lives, and why `ThreadLocal` values are retained: the map holding them hangs off the live thread. **Monitors.** An object a thread currently holds a lock on, or is waiting on, is treated as a root while that state persists. **VM-internal structures.** Class loader objects and the classes they define, resolved constant-pool entries including interned strings and class literals, method handles and call-site targets kept alive by `invokedynamic` bootstrapping, exceptions currently propagating, and anything pinned by a debugger or profiler through JVMTI. ## The class-loader corollary Statics being roots has a structural consequence: a class's static fields keep objects alive, the class keeps its defining class loader alive, and the loader keeps every class it defined alive. So one live reference to any object of any class defined by a loader can pin the entire loader, all its classes, and all their statics. That is the mechanism behind class-loader retention in redeployable containers, and it is why tools show the *path to a GC root* rather than merely a count when you ask why something survives. ## Not roots An ordinary instance field is not a root; it participates in the trace but only matters if its owner is reachable. Objects referenced only by other unreachable objects are not roots by any route. Locals of a frame that has already returned are gone with the frame. And a soft, weak, or phantom reference object is itself an ordinary object; its referent is reached only through a special, collector-controlled edge rather than a strong root path. ## Practical use of this list When a heap analysis tool explains why an object survives, it computes a path from a GC root and labels the root by category. Reading that label is the fastest diagnosis available: a static-field root points at a class-level registry, a Thread root at a running executor or a `ThreadLocal`, a JNI global at native code, and a Java local means an in-flight computation is simply still using the object.

  • Is an ordinary instance field a GC root?
    No. It is a heap-to-heap edge followed during tracing, and it keeps its target alive only if the object owning the field is itself reachable. Roots come from outside the heap graph: execution state, class storage, native handles, and VM structures.
  • Why can a single leaked object pin an entire class loader and all its classes?
    The object references its class, the class references its defining loader, and the loader references every class it defined, each of which owns its static fields. Because static fields are roots for the class's lifetime, retaining the loader retains that whole set, which is how class-loader retention shows up as a large unexplained metadata and heap footprint.

saying these in an interview costs you the question

  • Listing all heap objects, or any object with an incoming reference, as roots
  • Forgetting thread stacks, the most dynamic and important root source
  • Overlooking JNI global references and then being unable to explain a retention path from native code
  • Assuming static fields are released when application code stops using them, rather than when their class loader dies
  • Calling a WeakReference's referent a root because the reference object itself is reachable

context

open as a page

Some managed runtimes decide object liveness by reference counting. Why does the JVM use tracing from GC roots instead?

level: middleimportance: should knowfreq 42%

basics

~20 s

Reference counting cannot reclaim cycles: mutually referencing objects keep each other's counts above zero forever. It also charges every reference write with a count update, which must be atomic under concurrency. Tracing collects cycles naturally and costs proportional to live data.

open as a page

When a JVM collector scans a thread's stack for GC roots, how does it know which stack slots hold object references rather than ints or floats, and why must that scanning happen at a safepoint?

level: seniorimportance: should knowfreq 28%

basics

~20 s

HotSpot is precise: the interpreter and the JIT publish oop maps saying which stack slots and registers hold references at specific code positions. Those maps are valid only at those positions, so threads must be paused exactly there — that is what a safepoint is.

open as a page