Fundamentals 15 min read

Why Circular References Don't Leak Memory in Java: Reachability Analysis Explained

This article explains how Java's reachability analysis garbage collection handles circular references, contrasts it with reference counting's failure on cycles, details GC roots, three-color marking, finalize() resurrection, and identifies real memory leak patterns caused by lingering root references.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Why Circular References Don't Leak Memory in Java: Reachability Analysis Explained

Reference Counting Fails on Circular References

Reference counting maintains a counter per object; increment on reference, decrement on release, collect at zero. In a cycle (A references B, B references A), both counters stay at 1 after external references drop, preventing collection. This is not theoretical: C++ std::shared_ptr leaks on mutual ownership, and Python supplements reference counting with a generational collector to break cycles.

Why JVM Uses Reachability Analysis

JVM avoids reference counting for two reasons: (1) every assignment requires atomic counter updates (CAS), making writes expensive; (2) it cannot handle cycles. JVM trades brief Stop-The-World pauses for higher throughput, computing liveness via reachability in a single traversal.

Reachability Analysis: Graph Traversal from GC Roots

Objects and references form a directed graph. GC Roots are starting nodes. Traversal marks all reachable objects as live; unreachable objects are garbage. The algorithm ignores reference counts—it only checks connectivity from roots. A cycle disconnected from roots is never visited and collected.

Reference strength determines collectibility after reachability:

Strong reference ( Object o = new Object()): never collected while reachable.

Soft reference ( SoftReference): collected only before OutOfMemoryError; suitable for memory-sensitive caches.

Weak reference ( WeakReference): collected on next GC regardless of memory; used in WeakHashMap keys.

Phantom reference ( PhantomReference): get() returns null; used with ReferenceQueue for cleanup notifications (e.g., DirectByteBuffer).

GC Roots and OopMap

Root categories:

VM stack local variables : Method parameters, local objects in stack frames

Native method stack JNI references : Objects passed via JNI

Method area static fields : Class static fields

Method area runtime constants : String constant pool entries

Synchronized locks : Objects held by synchronized monitors

Temporary roots : Extra roots for generational/incremental correctness

HotSpot uses OopMap at safe points to record exact reference locations in stack frames and registers, avoiding full-stack scans. Threads must reach safe points before GC, necessitating Stop-The-World.

Three-Color Marking and Two-Phase Collection

First phase: traverse from roots using three-color marking—white (unvisited), gray (processing), black (processed). Remaining white objects are candidates for collection.

Second phase: objects with overridden finalize() not yet run are placed in F-Queue for a low-priority finalizer thread. They may resurrect by attaching to a live reference; if still unreachable after finalize(), they are collected. finalize() runs at most once and is deprecated since Java 9; use try-with-resources or explicit close().

Concurrent marking uses write barriers to prevent missing updates: Incremental Update (re-gray white objects inserted into black objects) and SATB (record broken references). G1 uses SATB; ZGC uses colored pointers with read barriers.

Circular References Are Collected Normally

After a = null; b = null;, the cycle has no path from any GC Root. Traversal never enters the cycle; both objects remain white and are collected. The article provides a runnable demo ( CircleGcDemo) that creates a 1 MB cycle, nulls external references, triggers GC, and observes finalize() resurrection once, then final collection on second GC. Expected output:

finalize 执行,对象复活
第一次 GC 后 hook 是否为空: false
第二次 GC 后 hook 是否为空: true

Weak references ( WeakReference<Node>) structurally prevent cycles from retaining objects because they are not strong roots.

Where Reachability Analysis Cannot Help: Real Memory Leaks

Reachability guarantees unreachable objects are collected; it cannot reclaim objects that remain reachable but unused. Common leak patterns:

ThreadLocal not removed : ThreadLocalMap keys are weak, values strong. Long-lived thread-pool threads retain values indefinitely.

Static collections only grow : static Map<String, Order> CACHE holds references forever.

Unregistered listeners/callbacks : Event sources keep strong references to listeners.

Unclosed resources : DB connections, streams held by long-lived objects.

All share the same root cause: a root reference chain that should have been severed was not.

Troubleshooting steps: jstat -gc <pid> to see old-gen growth after Full GC. jmap -dump:live,format=b,file=heap.hprof <pid> for heap dump.

Load into Eclipse MAT, use Path to GC Roots to trace retaining references (static fields, ThreadLocal entries).

Comparison: Reference Counting vs Reachability Analysis

Acyclic objects : Reference counting collects when count reaches zero; reachability collects when detached from roots.

Mutual references (cycle) : Reference counting count stuck at 1 → leak; reachability whole cycle detached → collected.

Object still rooted but unused : Both leak (algorithm considers it live).

Circular references in Java do not leak because reachability analysis checks connectivity, not indegree; real Java leaks almost always stem from forgetting to break a root reference that should have been released.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMGarbage CollectionReference CountingFinalizeMemory LeaksReachability AnalysisGC RootsCircular ReferencesThree-Color Marking
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.