Question 5 · Section 3

When does an object become eligible for GC?

An object becomes eligible for removal when it cannot be reached from any GC Root.

Language versions: English Russian Ukrainian

Junior Level

An object becomes eligible for removal when it cannot be reached from any GC Root.

GC Roots — entry points from which GC starts traversal: local variables in the stack, static fields, active threads, JNI references. If an object is unreachable from GC Roots — it’s garbage, even if there are references from other unreachable objects (“islands of isolation”).

Simple rule: No path from GC Root → object is garbage → GC will remove it.

Example:

public void example() {
    String s = new String("hello");  // Object created
    s = null;                        // Reference removed → object became garbage!

    String s2 = new String("world"); // New object
    // When method finishes → s2 removed from Stack
    // → object "world" is no longer accessible → garbage!
}

GC Roots — starting points:

  • Local variables in Stack
  • Static fields of classes
  • Active threads

If an object cannot be reached from GC Roots → it’s garbage.


Middle Level

Reachability Analysis

Java uses graph traversal from GC Roots, not reference counting (like Python):

GC Roots:
├── Stack variables (local variables of threads)
├── Static fields of classes
├── Active threads (Thread objects)
├── JNI references (from native code)
└── JVM internal objects

Object is alive if:
  There is a path from any GC Root → object

Object is garbage if:
  No path from GC Root → object

Why not Reference Counting?

// Reference Counting (Python) doesn't work for cycles:
class Node { Node next; }

Node a = new Node();
Node b = new Node();
a.next = b;  // a → b
b.next = a;  // b → a

a = null;
b = null;

// Reference Counting:
//   a.refCount = 1 (b references a)
//   b.refCount = 1 (a references b)
//   → Never deleted! "Island of Isolation"

// Reachability Analysis (Java):
//   No path from GC Roots → a and b → both deleted! ✅

JIT Reachability Optimization

public void process() {
    HeavyResource res = new HeavyResource();
    res.doAction();

    // JIT sees: 'res' is no longer used
    // → Object can be removed by GC right now!
    // Even if process() method is still running!

    doSomethingElse();  // res may already be deleted
}

Reference Types (java.lang.ref)

Type When removed Use case
Strong Never (normal references) Business objects
Soft When memory is low Caches
Weak At next collection WeakHashMap
Phantom After finalization Resource cleanup
// Soft Reference — for caches
SoftReference<ExpensiveObject> ref = new SoftReference<>(obj);
ExpensiveObject cached = ref.get();  // null if GC removed

// Weak Reference — for WeakHashMap
WeakReference<User> ref = new WeakReference<>(user);
// GC will remove at next collection if no Strong references

Common mistakes

  1. Forgetting to remove reference
    // ❌ Object won't be deleted while list is alive
    static List<Object> cache = new ArrayList<>();
    cache.add(expensiveObject);  // Forgot to remove → leak!
    
  2. finalize() — object “resurrection”
    // ❌ In finalize() you can assign this to a static variable
    // → Object will "resurrect"!
    protected void finalize() {
        GlobalCache.add(this);  // Resurrection!
    }
    

    // finalize() is called only once. If you assign this to a static // field — the object becomes reachable again. This is called “resurrection”. // In Java 9+ finalize() is deprecated — use Cleaner.


Senior Level

GC Roots: Full Classification

GC Roots include:
├── Stack Locals (local variables of active methods)
├── Stack Parameters (parameters of methods in stack)
├── Static Fields (static fields of loaded classes)
├── JNI Global References (global references from native code)
├── JNI Local References (local references in JNI methods)
├── Thread Objects (Thread objects themselves — while not terminated)
├── Monitor Used (objects in synchronized/wait)
├── System Dictionary (loaded classes)
├── JVM Internal (preallocated exceptions, ClassLoaders)
└── Unreachable Finalizer Queue (objects in finalization queue)

JIT Reachability: reachabilityFence

// Problem: object can be deleted during native code execution
public void nativeAction() {
    NativeResource res = new NativeResource();
    res.doNativeOperation();  // Native operation (long-running)

    // JIT sees: 'res' not used after the call
    // → May delete res BEFORE native operation completes!
    // → Native code will crash!
}

// Solution: reachabilityFence (Java 9+)
public void nativeAction() {
    NativeResource res = new NativeResource();
    res.doNativeOperation();
    Reference.reachabilityFence(res);  // Guarantee: res alive until here!
}

Reference API Deep Dive

// Soft Reference — cleanup formula
// JVM cleans when:
//   idle_time < free_memory × SoftRefLRUPolicyMSPerMB
// Default: 1000 ms per each MB of free heap

// On large heaps (32 GB):
//   32 GB × 1000 ms/MB = 32,000 seconds = 9 hours!
//   → SoftReference can live for hours after "death"

// Tuning: -XX:SoftRefLRUPolicyMSPerMB=100
//   → Cleanup at 100 ms/MB → more aggressive

// Weak Reference — cleaned at NEXT collection
// → Doesn't wait for memory shortage!

// Phantom Reference — get() always returns null
// → Used ONLY for tracking removal
// → Replaces finalize()

Reference Handler Thread

JVM has a high-priority thread — Reference Handler

When GC detects reachability change:
  1. Object placed in Pending List
  2. Reference Handler extracts from Pending List
  3. Places in ReferenceQueue (specified at creation)
  4. Application polls queue → executes logic

→ Asynchronous mechanism, doesn't block GC

Cleaner (Java 9+)

// Replacement for finalize()
public class NativeResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final NativeHandle handle;

    public NativeResource() {
        this.handle = nativeAllocate();
        // Lambda does NOT have reference to this → no "resurrection"!
        this.cleanable = CLEANER.register(this, () -> {
            nativeFree(handle);  // Native memory cleanup
        });
    }

    @Override
    public void close() {
        cleanable.clean();  // Manual cleanup
    }
}

// On GC: if no Strong references → Cleaner calls lambda
// On close(): manual cleanup without waiting for GC

Production Experience

Real scenario: JIT deleted object too early

  • JNI library: native operation 100 ms
  • Java object deleted by GC after native method call
  • Native code accesses freed memory → SEGFAULT
  • Solution: reachabilityFence(obj) after native call

Best Practices

  1. Don’t rely on finalize() — use Cleaner/try-with-resources
  2. SoftReference for caches, but tune SoftRefLRUPolicyMSPerMB Don’t use SoftReference as the only caching mechanism in production — use Caffeine/Guava with explicit limits. SoftReference can cause excessive GC pressure under load.
  3. WeakReference for metadata and registries
  4. reachabilityFence for native operations
  5. Clean references in static collections
  6. Monitor ReferenceQueue for phantom references
  7. Avoid object “resurrection”

Senior Summary

  • Reachability Analysis = graph traversal from GC Roots
  • Reference Counting not used (cycle problem)
  • JIT can delete object BEFORE method completes
  • reachabilityFence (Java 9+) protects from premature removal
  • SoftReference = caches, cleanup on low memory
  • WeakReference = metadata, cleanup at next collection
  • PhantomReference + Cleaner = finalize() replacement
  • Reference Handler = async thread for reference processing

Interview Cheat Sheet

Must know:

  • Object is eligible for removal when unreachable from GC Roots (no path from any root)
  • Java uses Reachability Analysis (graph traversal), not Reference Counting — this solves the “island of isolation” problem
  • JIT can remove an object before method completes if it sees it’s no longer used
  • 4 reference types: Strong (never), Soft (on low memory), Weak (at next GC), Phantom (after finalization)
  • finalize() deprecated since Java 9 — use Cleaner or try-with-resources
  • reachabilityFence(obj) guarantees object won’t be removed during native operation
  • Object resurrection in finalize() — anti-pattern; Cleaner cannot resurrect (lambda has no reference to this)

Common follow-up questions:

  • Why doesn’t Java use Reference Counting? — Doesn’t detect cyclic references (a→b→a), overhead on every assignment
  • What is “island of isolation”? — Group of objects referencing each other but unreachable from GC Roots
  • When is SoftReference cleaned? — On memory shortage; formula depends on SoftRefLRUPolicyMSPerMB (default 1000 ms/MB)
  • Why does PhantomReference.get() always return null? — Specifically designed only for tracking object removal fact

Red flags (DO NOT say):

  • “Object is deleted when all references are null” — reachability from GC Roots matters, not null references
  • “finalize() is a good way to free resources” — deprecated, unpredictable, can resurrect object
  • “JIT doesn’t affect object lifetime” — JIT can remove object before method completes
  • “GC removes object immediately after obj = null” — removal happens at next collection if object is unreachable

Related topics:

  • [[4. What is Garbage Collection]]
  • [[6. What is a memory leak in Java]]
  • [[25. What are GC roots]]
  • [[26. What is reachability in the context of GC]]
  • [[27. Can you manually invoke GC]]