AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

16 Memory and the JVM runtime / Lesson 2 of 6

Object lifecycle, reachability and reference types

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is a memory leak in Java?

A memory leak in Java is an object that is still reachable from a GC root but will never be used again, so the garbage collector must keep it. The usual causes are static collections that only grow, listeners never removed, ThreadLocal values in thread pools and unclosed resources. The fix is to remove the reference when done, or hold it through a reference type the collector may clear.

An object is born at new, lives while something can reach it, and is reclaimed some time after nothing can. Java has no delete; you cannot free an object, you can only stop referring to it. That single rule — reachability, not reference counting — decides what a "memory leak" means in Java, why cycles are harmless, and why WeakHashMap and SoftReference exist. This lesson follows an object from allocation to collection and introduces the four strengths of reference.

Reachability and GC roots

The collector starts from the roots: every local variable and operand on every thread's stack, every static field of every loaded class, JNI handles, and a few runtime internals. Anything a root points to is live; anything a live object points to is live; and so on transitively. Everything else is garbage, no matter how many other garbage objects point to it.

Node a = new Node(); Node b = new Node();
a.next = b; b.next = a;      // a cycle
a = null; b = null;          // nothing on the stack reaches the pair: both are garbage

Reference counting would keep that cycle alive forever; tracing from roots does not — the count of incoming references is irrelevant, only the path from a root matters. This is the first thing to say when asked "does Java leak with circular references?": no.

What a leak is, then

A Java leak is an object that is reachable but never used again. The collector cannot tell the difference between "still needed" and "forgotten in a collection", so the classic leaks are all forms of forgetting:

  • A static List or Map that only ever grows (a cache with no eviction, a registry nothing unregisters from).
  • Listeners and callbacks registered with a long-lived object and never removed — the subject keeps the observer alive, and the observer keeps everything it references alive.
  • ThreadLocal values in a thread pool: the thread outlives the request, so does the value, unless remove() is called.
  • Inner-class instances holding an implicit this to a large outer object (a lambda or anonymous class stored somewhere long-lived).
  • Substrings before Java 7u6 shared the parent's char[]; a habit of keeping a two-character substring of a 10 MB string kept the 10 MB. Fixed now — but the shape of the bug, a small handle retaining a large structure, recurs everywhere (an Iterator kept alive, a Map.Entry, a view from subList).
  • Unclosed resources: a FileInputStream that is not closed holds a native file descriptor the collector may take minutes to release.

The fix is always the same: remove the reference when you are done, or use a reference type the collector may clear.

The four reference strengths

ReferenceCollected when…Typical use
Strong (Object o = …)never, while reachableeverything by default
SoftReference<T>memory is short — cleared before an OutOfMemoryErrormemory-sensitive caches
WeakReference<T>the next GC finds no strong refscanonicalising maps, listener lists, WeakHashMap keys
PhantomReference<T>after finalization, before memory reuseresource cleanup scheduling (with a ReferenceQueue)
WeakReference<byte[]> ref = new WeakReference<>(new byte[1_000_000]);
byte[] data = ref.get();       // the array, or null if it has been collected

get() on a weak or soft reference returns null once the collector has cleared it; code using them must always handle that. A ReferenceQueue passed to the constructor is where the reference object itself is enqueued after clearing, so a cache can learn that an entry died without polling.

WeakHashMap holds its keys weakly: when a key is no longer strongly referenced elsewhere, the entry disappears at the next collection. It is right for metadata attached to objects you do not own (a map from Thread to some per-thread info) and wrong for anything whose keys are String literals or Integers — those are interned or cached and never become unreachable, so the map never shrinks.

Soft references are a tempting cache but a poor one in practice: the JVM clears them all at once under pressure, giving a cache that is either full or empty. A bounded LRU (LinkedHashMap with removeEldestEntry, lesson 6) or a real cache library is nearly always better.

Finalization is gone; use Cleaner or try-with-resources

Object.finalize() was deprecated in Java 9 and marked for removal in 18: it ran at an unpredictable time on an unspecified thread, could resurrect objects, slowed collection, and an exception in it was silently swallowed. Resource cleanup is the job of AutoCloseable + try-with-resources, deterministic and immediate. For the rare case where a class must clean up native memory even if a careless caller forgets close(), java.lang.ref.Cleaner registers a cleanup action that runs after the object becomes phantom-reachable:

class Handle implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    Handle(long nativePtr) { this.cleanable = CLEANER.register(this, () -> free(nativePtr)); }   // the action must not capture `this`
    @Override public void close() { cleanable.clean(); }
}

The safety net runs eventually; close() runs now. Always provide and prefer close().

The lifecycle, end to end

  1. Allocation — new bumps a pointer in the thread's local allocation buffer (TLAB): a few instructions, no lock. This is why "allocation is expensive" is folklore from another language; in Java it is collection that costs, in proportion to how many objects survive.
  2. Use — the object is strongly reachable from a root.
  3. Unreachable — the last strong reference is overwritten or its frame is popped. Nothing happens yet.
  4. Discovered — a collection runs, traces from roots, does not find the object. Weak references to it are cleared and enqueued.
  5. Reclaimed — its memory is reused. If it had a Cleaner, the action runs on the cleaner thread.

Between 3 and 5 the memory is still occupied; System.gc() suggests a collection and is ignored by many collectors (-XX:+DisableExplicitGC). Code that depends on an object being collected at a particular moment is wrong by design.

Interview angle

  • "How does Java decide what to collect?" Reachability from GC roots — stack locals, statics, JNI handles — traced transitively; not reference counts.
  • "Can circular references leak?" No; an unreachable cycle is garbage.
  • "What is a memory leak in Java?" A reachable object no one will use again — static collections, unremoved listeners, ThreadLocals in pools.
  • "Weak versus soft reference?" Weak: cleared at the next GC once unreferenced. Soft: kept until memory is short.
  • "Why not finalize()?" Unpredictable, slow, unsafe, deprecated; use try-with-resources, and Cleaner as a last-resort safety net.

Key takeaways

  • Live = reachable from a root; everything else is garbage, cycles included.
  • Leaks are forgotten references: static collections, listeners, ThreadLocals, small handles to big structures.
  • Strong / soft / weak / phantom — decreasing strength, increasing willingness to be cleared; get() may return null.
  • WeakHashMap weak-keys; useless with interned or cached keys.
  • No finalize; AutoCloseable now, Cleaner as the safety net; never rely on when a collection happens.

Common questions

Can circular references cause a memory leak in Java?

No. The garbage collector traces reachability from GC roots — stack locals, static fields, JNI handles — instead of counting references, so a cycle of objects that no root can reach is garbage and is collected like any other.

What is the difference between weak and soft references in Java?

A WeakReference is cleared at the next garbage collection once no strong reference to its object remains. A SoftReference is kept until memory runs short and is cleared before an OutOfMemoryError. With both, get() returns null once the reference has been cleared.

What are GC roots in Java?

GC roots are where the collector's trace starts: the local variables and operands on every thread's stack, the static fields of loaded classes, JNI handles and a few runtime internals. Anything reachable from a root is live; everything else is garbage.

When should I use WeakHashMap?

Use WeakHashMap for metadata attached to objects you do not own: it holds its keys weakly, so an entry disappears once its key is no longer strongly referenced elsewhere. It never shrinks with String literal or small Integer keys, because those are pooled or cached and stay reachable.

Why is finalize() deprecated in Java?

finalize() ran at an unpredictable time on an unspecified thread, could resurrect objects, slowed collection and silently swallowed exceptions. It was deprecated in Java 9 and marked for removal in Java 18; clean up with AutoCloseable and try-with-resources, and use Cleaner only as a safety net.

Mark as readSign in to keep your place.

Memory Leaks in Java: Weak, Soft and Phantom References

Java collects every object unreachable from GC roots, so cycles never leak. What a Java memory leak really is, and strong, soft, weak and phantom references.

  • Course: Java study plan
  • Module: Memory and the JVM runtime
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

What is a memory leak in Java?

A memory leak in Java is an object that is still reachable from a GC root but will never be used again, so the garbage collector must keep it. The usual causes are static collections that only grow, listeners never removed, ThreadLocal values in thread pools and unclosed resources. The fix is to remove the reference when done, or hold it through a reference type the collector may clear.

Lesson

An object is born at new, lives while something can reach it, and is reclaimed some time after nothing can. Java has no delete; you cannot free an object, you can only stop referring to it. That single rule — reachability, not reference counting — decides what a "memory leak" means in Java, why cycles are harmless, and why WeakHashMap and SoftReference exist. This lesson follows an object from allocation to collection and introduces the four strengths of reference.

Reachability and GC roots

The collector starts from the roots: every local variable and operand on every thread's stack, every static field of every loaded class, JNI handles, and a few runtime internals. Anything a root points to is live; anything a live object points to is live; and so on transitively. Everything else is garbage, no matter how many other garbage objects point to it.

Node a = new Node(); Node b = new Node();
a.next = b; b.next = a;      // a cycle
a = null; b = null;          // nothing on the stack reaches the pair: both are garbage

Reference counting would keep that cycle alive forever; tracing from roots does not — the count of incoming references is irrelevant, only the path from a root matters. This is the first thing to say when asked "does Java leak with circular references?": no.

What a leak is, then

A Java leak is an object that is reachable but never used again. The collector cannot tell the difference between "still needed" and "forgotten in a collection", so the classic leaks are all forms of forgetting:

  • A static List or Map that only ever grows (a cache with no eviction, a registry nothing unregisters from).
  • Listeners and callbacks registered with a long-lived object and never removed — the subject keeps the observer alive, and the observer keeps everything it references alive.
  • ThreadLocal values in a thread pool: the thread outlives the request, so does the value, unless remove() is called.
  • Inner-class instances holding an implicit this to a large outer object (a lambda or anonymous class stored somewhere long-lived).
  • Substrings before Java 7u6 shared the parent's char[]; a habit of keeping a two-character substring of a 10 MB string kept the 10 MB. Fixed now — but the shape of the bug, a small handle retaining a large structure, recurs everywhere (an Iterator kept alive, a Map.Entry, a view from subList).
  • Unclosed resources: a FileInputStream that is not closed holds a native file descriptor the collector may take minutes to release.

The fix is always the same: remove the reference when you are done, or use a reference type the collector may clear.

The four reference strengths

ReferenceCollected when…Typical use
Strong (Object o = …)never, while reachableeverything by default
SoftReference<T>memory is short — cleared before an OutOfMemoryErrormemory-sensitive caches
WeakReference<T>the next GC finds no strong refscanonicalising maps, listener lists, WeakHashMap keys
PhantomReference<T>after finalization, before memory reuseresource cleanup scheduling (with a ReferenceQueue)
WeakReference<byte[]> ref = new WeakReference<>(new byte[1_000_000]);
byte[] data = ref.get();       // the array, or null if it has been collected

get() on a weak or soft reference returns null once the collector has cleared it; code using them must always handle that. A ReferenceQueue passed to the constructor is where the reference object itself is enqueued after clearing, so a cache can learn that an entry died without polling.

WeakHashMap holds its keys weakly: when a key is no longer strongly referenced elsewhere, the entry disappears at the next collection. It is right for metadata attached to objects you do not own (a map from Thread to some per-thread info) and wrong for anything whose keys are String literals or Integers — those are interned or cached and never become unreachable, so the map never shrinks.

Soft references are a tempting cache but a poor one in practice: the JVM clears them all at once under pressure, giving a cache that is either full or empty. A bounded LRU (LinkedHashMap with removeEldestEntry, lesson 6) or a real cache library is nearly always better.

Finalization is gone; use Cleaner or try-with-resources

Object.finalize() was deprecated in Java 9 and marked for removal in 18: it ran at an unpredictable time on an unspecified thread, could resurrect objects, slowed collection, and an exception in it was silently swallowed. Resource cleanup is the job of AutoCloseable + try-with-resources, deterministic and immediate. For the rare case where a class must clean up native memory even if a careless caller forgets close(), java.lang.ref.Cleaner registers a cleanup action that runs after the object becomes phantom-reachable:

class Handle implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    Handle(long nativePtr) { this.cleanable = CLEANER.register(this, () -> free(nativePtr)); }   // the action must not capture `this`
    @Override public void close() { cleanable.clean(); }
}

The safety net runs eventually; close() runs now. Always provide and prefer close().

The lifecycle, end to end

  1. Allocation — new bumps a pointer in the thread's local allocation buffer (TLAB): a few instructions, no lock. This is why "allocation is expensive" is folklore from another language; in Java it is collection that costs, in proportion to how many objects survive.
  2. Use — the object is strongly reachable from a root.
  3. Unreachable — the last strong reference is overwritten or its frame is popped. Nothing happens yet.
  4. Discovered — a collection runs, traces from roots, does not find the object. Weak references to it are cleared and enqueued.
  5. Reclaimed — its memory is reused. If it had a Cleaner, the action runs on the cleaner thread.

Between 3 and 5 the memory is still occupied; System.gc() suggests a collection and is ignored by many collectors (-XX:+DisableExplicitGC). Code that depends on an object being collected at a particular moment is wrong by design.

Interview angle

  • "How does Java decide what to collect?" Reachability from GC roots — stack locals, statics, JNI handles — traced transitively; not reference counts.
  • "Can circular references leak?" No; an unreachable cycle is garbage.
  • "What is a memory leak in Java?" A reachable object no one will use again — static collections, unremoved listeners, ThreadLocals in pools.
  • "Weak versus soft reference?" Weak: cleared at the next GC once unreferenced. Soft: kept until memory is short.
  • "Why not finalize()?" Unpredictable, slow, unsafe, deprecated; use try-with-resources, and Cleaner as a last-resort safety net.

Key takeaways

  • Live = reachable from a root; everything else is garbage, cycles included.
  • Leaks are forgotten references: static collections, listeners, ThreadLocals, small handles to big structures.
  • Strong / soft / weak / phantom — decreasing strength, increasing willingness to be cleared; get() may return null.
  • WeakHashMap weak-keys; useless with interned or cached keys.
  • No finalize; AutoCloseable now, Cleaner as the safety net; never rely on when a collection happens.

Common questions

Can circular references cause a memory leak in Java?

No. The garbage collector traces reachability from GC roots — stack locals, static fields, JNI handles — instead of counting references, so a cycle of objects that no root can reach is garbage and is collected like any other.

What is the difference between weak and soft references in Java?

A WeakReference is cleared at the next garbage collection once no strong reference to its object remains. A SoftReference is kept until memory runs short and is cleared before an OutOfMemoryError. With both, get() returns null once the reference has been cleared.

What are GC roots in Java?

GC roots are where the collector's trace starts: the local variables and operands on every thread's stack, the static fields of loaded classes, JNI handles and a few runtime internals. Anything reachable from a root is live; everything else is garbage.

When should I use WeakHashMap?

Use WeakHashMap for metadata attached to objects you do not own: it holds its keys weakly, so an entry disappears once its key is no longer strongly referenced elsewhere. It never shrinks with String literal or small Integer keys, because those are pooled or cached and stay reachable.

Why is finalize() deprecated in Java?

finalize() ran at an unpredictable time on an unspecified thread, could resurrect objects, slowed collection and silently swallowed exceptions. It was deprecated in Java 9 and marked for removal in Java 18; clean up with AutoCloseable and try-with-resources, and use Cleaner only as a safety net.

Exercises

Mark from the roots

Implement the mark phase. Read an integer n and n object names; an integer r and r root names; an integer e and e edges from to (object from holds a reference to to). Print live=<reachable objects in natural order, space-separated> and garbage=<the rest, natural order> (print - for an empty list). Cycles among unreachable objects are still garbage.

Example: 5 then a b c d e, 1 then a, 4 then a b, b c, d e, e d →

live=a b c
garbage=d e

A cache with weak entries

Simulate a map whose entries can be held strongly or weakly. Read an integer n and n commands: put <k> <v> strong|weak stores the entry with that strength (replacing any previous); gc removes every weak entry and prints gc cleared <count>; get <k> prints <k>=<v> or <k>=null. After all commands print size=<entries>.

Example: 6 then put a 1 strong, put b 2 weak, get b, gc, get b, get a →

b=2
gc cleared 1
b=null
a=1
size=1

In this module: Memory and the JVM runtime

← Stack and heap — where every value lives · Garbage collection — generations, pauses and the collectors →