🧠 Section 3 · Question #25

What are GC roots

A GC Root (Garbage Collection Root) is a fundamental anchor object in Java memory that is guaranteed to be reachable and serves as the starting point for the JVM's Tracing Garba...


🟢 Junior Level

A GC Root (Garbage Collection Root) is a fundamental anchor object in Java memory that is guaranteed to be reachable and serves as the starting point for the JVM’s Tracing Garbage Collector during Reachability Analysis.

The Family Tree Analogy

Imagine an inverted family tree. The roots at the top are the GC Roots. The Garbage Collector traverses downward along the branches (object references) to discover all attached leaves (objects in the heap). Any object that has an unbroken chain of strong references from at least one GC Root is considered reachable (live). Any disconnected object—or any isolated cycle of objects referencing each other—is declared unreachable (dead) and reclaimed.

[GC Root: Thread Stack Frame]
         │
         ▼
    [Object A] ──► [Object B] ──► [Object C]    <-- ALL ALIVE (Reachable from Root)

    [Object X] ◄───► [Object Y]                 <-- DEAD / GARBAGE (Unreachable from Root,
                                                     despite circular references)

Core GC Root Categories

  1. Thread Stack Variables: Local variables and method parameters inside currently executing stack frames.
  2. Static Fields: References stored in static variables of loaded classes (java.lang.Class instances).
  3. Active Thread Objects: All running java.lang.Thread instances registered in the JVM runtime.
  4. JNI References: Global and local pointers created inside native C/C++ code via the Java Native Interface.
  5. Synchronization Monitors: Objects currently locked inside synchronized blocks or waiting in monitor queues.

🟡 Middle Level

1. HotSpot Internal GC Root Classification

HotSpot organizes GC Roots into distinct, rigorously tracked runtime categories:

GC Root Category Description Storage Location
Stack Local Local variables and operand stack slots in active frames across all threads Operating System Thread Stack
Static Field Static references attached to loaded classes Java Heap (inside java.lang.Class mirror)
Active Thread Running java.lang.Thread objects registered in the JVM thread list Java Heap / JVM Internal Thread Table
JNI Global Native global references created via (*env)->NewGlobalRef JNI Global Reference Table
JNI Local Native method arguments and local references within C/C++ frames Native C-Stack of JNI Frame
Monitor Used Objects whose internal ObjectMonitor is locked by a thread JVM Monitor Synchronization List
JVM Internal System ClassLoaders (Bootstrap/Platform), interned StringTable, pre-allocated OutOfMemoryErrors JVM Native Runtime System Dictionary
+-------------------------------------------------------------------------+
|                         HOTSPOT GC ROOT TOPOLOGY                        |
+-------------------------------------------------------------------------+
|  [Thread Stacks]          [Class Static Fields]   [Native JNI References]
|  - Frame: orderRequest    - Configuration.CACHE   - NewGlobalRef(nativeHdl)
|  - Frame: userProfile     - DatabasePool.INSTANCE - JNI local frame ptrs 
|                                                                         
|  [JVM System Entities]    [Synchronization Locks]                       
|  - Bootstrap ClassLoader  - Objects locked in synchronized(mutex)       
|  - Interned String pool   - Objects waiting on monitor condition queues 
+-------------------------------------------------------------------------+

2. Exact Garbage Collection and JIT OopMaps

An OS thread stack consists of raw 64-bit memory slots. A slot can contain either a primitive 64-bit integer (long value 0x7fa281c00) or an object heap reference pointing to address 0x7fa281c00.

How does the Garbage Collector know whether a stack slot is a reference or a primitive?

  • Conservative GC (Legacy): Assumed any memory value matching a heap address range was an object pointer. This caused false memory retentions and prevented memory compaction.
  • Exact (Precise) GC (HotSpot): HotSpot knows with 100% precision which slots contain pointers.
  • During bytecode compilation, the JIT compiler produces OopMaps (Ordinary Object Pointer Maps) for every Safepoint instruction.
  • An OopMap is a compact bitmask detailing which CPU registers and stack frame offsets contain valid object pointers at that exact machine instruction.
  • During root scanning, the GC inspects only the designated OopMap slots, completing stack scanning in sub-millisecond time.

🔴 Senior Level

1. Scalability of Root Scanning and Thread-Local Handshakes

The initial phase of garbage collection—Root Scanning—traditionally contributed directly to the Stop-The-World (STW) pause duration:

\[\text{STW Pause Duration} \ge \text{Time to Safepoint (TTSP)} + \text{Root Scanning Duration}\]

If an application maintains 5,000 platform threads, scanning every thread’s stack frames sequentially can extend STW pauses by tens of milliseconds.

Thread-Local Handshakes (JEP 312, Java 10+)

Modern collectors (such as ZGC, Shenandoah, and modern G1) replace global STW root scanning with Thread-Local Handshakes:

  • Instead of halting all mutator threads simultaneously, the JVM pauses and scans thread stacks individually and asynchronously.
  • Thread A can have its stack roots scanned while Threads B, C, and D continue processing user HTTP requests concurrently.
  • In low-latency collectors (ZGC and Shenandoah), thread stack roots are scanned concurrently or during microsecond handshake pauses.

2. The Native Blind Spot: JNI Global Reference Leaks

One of the most dangerous and elusive memory leaks in enterprise systems occurs across the JNI boundary:

// Native C/C++ implementation:
JNIEXPORT void JNICALL Java_com_example_NativeBridge_initBridge(JNIEnv *env, jobject caller, jobject listener) {
    // Elevates Java object to a permanent JVM GC Root!
    jobject globalListener = (*env)->NewGlobalRef(env, listener);
    
    // If the native developer forgets to invoke (*env)->DeleteGlobalRef(env, globalListener),
    // the listener object is permanently pinned as an immortal GC Root.
}

In Eclipse MAT, these appear under the category JNI Global. Because the reference exists in native C-heap memory managed outside Java bytecode, Java static code analyzers cannot detect the leak.


3. The Metaspace Myth: Where Are Static Fields Stored?

[!IMPORTANT] Common Interview Fallacy: “Static fields are stored in Metaspace.” HotSpot Reality: Since Java 7 (and permanently solidified in Java 8 with the elimination of PermGen), all static object references and primitive static variables are stored in the Java Object Heap inside the java.lang.Class mirror object.

Metaspace resides in off-heap native memory and holds only C++ VM metadata (Klass structures, vtables, method bytecode, annotations). The java.lang.Class instance resides in the Java heap and acts as the official GC Root for static fields.


4 Tricky Questions

1. Where are static class variables physically stored in HotSpot, and why does this make them GC Roots?

Answer: Static variables are physically stored on the Java Heap inside the java.lang.Class mirror instance allocated for that loaded class. They are not stored in Metaspace (which only contains native C++ metadata structures such as InstanceKlass).

The java.lang.Class instance is pinned in memory by its defining ClassLoader, which is registered in the JVM’s System Dictionary. Because the ClassLoader is an active system entity, it serves as an immortal root, keeping the java.lang.Class instance alive. The static fields within that Class instance directly reference other objects in the heap, anchoring them as reachable GC Roots.


2. How does the JVM distinguish between a 64-bit primitive integer long and an object reference pointer on the thread stack during GC root scanning?

Answer: HotSpot is an Exact (Precise) Garbage Collector. It does not inspect the contents of stack memory to guess if a value looks like a pointer.

Instead, during bytecode execution and JIT compilation (C1 / C2), the compiler emits metadata called OopMaps (Ordinary Object Pointer Maps) for every Safepoint instruction. An OopMap records the exact state of the CPU registers and thread stack offsets. When a thread pauses at a Safepoint, the GC consults the corresponding OopMap, which explicitly flags: “Offset +16 is a pointer; Offset +24 is an uncompressed 64-bit integer.” The GC scans only the verified pointer offsets, completely avoiding false-pointer errors.


3. Can an object be collected if all references in Java code are set to null, but an active thread is holding a synchronized lock on it?

Answer: No, the object cannot be garbage collected.

When a thread enters a synchronized (mutex) block, the JVM associates an internal ObjectMonitor structure with the object (inflating the object’s mark word). As long as the thread remains inside the synchronized block or is waiting on that monitor (mutex.wait()), the object is registered in the JVM’s internal list of active monitors. The JVM treats all locked objects as Monitor Used GC Roots. Only after the thread exits the synchronized block and releases the monitor does the object lose its root status.


4. How does having 10,000 operating system platform threads impact the Garbage Collector’s root scanning phase, and how do Virtual Threads change this equation?

Answer:

  • Platform Threads (OS Threads): Each thread allocates a dedicated native OS stack ($\sim 1\text{ MB}$). During a GC cycle, the collector must scan the stack frames of all 10,000 threads. Even with OopMaps, inspecting tens of thousands of active stack frames scales linearly ($O(N)$ with thread count), causing substantial Stop-The-World root scanning delays.
  • Virtual Threads (Project Loom, Java 21+): Virtual threads do not possess dedicated OS call stacks. When a virtual thread unmounts (e.g., during blocking I/O), its stack frames are preserved as standard heap objects (Continuation / StackChunk instances) inside the Java Heap. The GC treats them as regular heap objects rather than root frames. The GC Root set is reduced to only the small pool of underlying carrier platform threads (typically equal to the host CPU core count), eliminating the massive root-scanning latency overhead.

🎯 Interview Cheat Sheet

30-Second Summary

“GC Roots are the foundational live anchors from which the JVM traverses the reachability graph during garbage collection. The primary root categories include: local variables and parameters on active thread stacks, static fields of loaded classes, active running thread objects, JNI global/local native references, and objects locked in synchronized blocks. HotSpot uses JIT-compiled OopMaps to identify stack pointers with 100% precision, while modern JVMs employ Thread-Local Handshakes to scan thread roots concurrently without global multi-thread freezes.”

Common Follow-up Questions

  • “How do cyclic references get collected?” → If objects A and B reference each other in a closed loop, but neither has an unbroken path from any GC Root, both are unreachable and reclaimed simultaneously.
  • “Why should you exclude Weak and Soft references when inspecting GC Root paths in Eclipse MAT?” → Because Weak and Soft references do not prevent an object from being reclaimed during memory pressure. Only Strong references keep a memory leak alive.
  • “Can a ClassLoader be garbage collected?” → Yes, but only if all classes loaded by it, all instances of those classes, and the ClassLoader itself are completely unreachable from all GC Roots.

Red Flags (DO NOT Say)

  • ❌ “GC Roots are just static variables.” (Stack locals, running threads, JNI handles, and monitor locks are all primary GC Roots).
  • ❌ “The GC inspects every memory byte and guesses what is a pointer.” (HotSpot is an Exact GC that uses JIT OopMaps to locate references instantly).
  • ❌ “Static fields are stored in Metaspace.” (Metaspace stores native C++ metadata; static variables reside on the Java Heap inside java.lang.Class).