Understanding JVM Reference Types and Their Garbage Collection Behavior
JVM Reference Types Overview
Java provides different reference types that allow developers to control the relationship between objects and garbage collection. These reference types are particularly important when working with memory-sensitive applications where you need fine-grained control over object lifecycle management.
A common interview question involves explaining the diffferences between strong, soft, weak, phantom, and finalizer references, and when to use each type. Since Java 1.2, the reference framwork was expanded to include four distinct reference categories with progressively weaker guarantees about object retention.
Except for finalizer references, the three main reference types are accessible through the java.lang.ref package and can be used directly in applications. The finalizer reference is package-private and primarily used internally by the JVM.
Understanding reference behavior requires knowing that garbage collection characteristics apply when reference relationships still exist:
Strong Reference: The default reference type in Java. As long as a strong reference to an object exists, the garbage collector will never collect it. An out-of-memory error occurs before a strong reference is cleared.Soft Reference: Objects reachable only through soft references are retained when memory is insufficient. The garbage collector collects these before throwing an OOM exception.Weak Reference: Objects with only weak references are collected during the next garbage collection cycle, regardless of available memory.Phantom Reference: Cannot prevent collection and provides no access to the referenced object. Its primary purpose is tracking when an object is about to be collected.Strong References: Never Collected While Referenced
Strong references are the most common reference type in Java applications, accounting for the vast majority of object references in typical code. When you use the new operator to create an object and assign it to a variable, that variable holds a strong reference to the newly created object.
Objects with active strong references are considered reachable, and the garbage collector will not collect them under any circumstances. An object becomes eligible for collection only when it goes out of scope or when you explicitly assign null to the reference variable. The actual collection timing depends on the specific garbage collection strategy in use.
Unlike soft, weak, and phantom references, which allow objects to be collected under certain conditions, strong references can lead to memory leaks if not managed properly. This is why understanding reference types is crucial for building memory-efficient applications.
Example implementation:
public class StrongReferenceDemo {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder("Hello, World");
StringBuilder primaryRef = builder;
builder = null;
System.gc();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(primaryRef);
}
}
In this example, builder points to a StringBuilder instance on the heap. When primaryRef = builder executes, both variables reference the same object. After setting builder = null, the object remains accessible through primaryRef.
Strong references have these key characteristics:
Provide direct access to the referenced objectPrevent collection entirely as long as they existCan cause memory leaks when held longer than necessarySoft References: Collected When Memory Runs Low
Soft references are designed for objects that are useful but not critical. An object accessible only through soft references is eligible for collection when the JVM is low on memory, specificlaly before throwing an OutOfMemoryError.
This reference type is commonly used for implementing memory-sensitive caches. The cache can retain entries as long as sufficient memory is available, but when memory pressure increases, the garbage collector can reclaim these cached entries to free up resources.
When the garbage collector decides to clear a soft reference, it optionally stores the reference in a reference queue for post-processing. The JVM attempts to keep soft references alive longer than weak references, only clearing them when memory becomes genuinely constrained.
Soft reference behavior:
Memory sufficient: Soft-reachable objects are preservedMemory insufficient: Soft-reachable objects are collected
Java provides the SoftReference class since JDK 1.2 for implementing soft references.
Object strongRef = new Object();
SoftReference<Object> softRef = new SoftReference<Object>(strongRef);
strongRef = null;
Test implementation:
/**
* Soft reference demonstration
* JVM args: -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class SoftReferenceDemo {
public static class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
Person person = new Person(1, "johndoe");
SoftReference<Person> personRef = new SoftReference<>(person);
person = null;
System.out.println("Before GC: " + personRef.get());
System.gc();
System.out.println("After GC: " + personRef.get());
try {
byte[] buffer = new byte[1024 * 7168 - 399 * 1024];
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("After memory pressure: " + personRef.get());
}
}
Weak References: Collected on Next GC Cycle
Weak references, like soft references, do not prevent their referents from being collected. However, an object reachable only through weak references is guaranteed to be collected during the next garbage collection pass, regardless of available memory.
The garbage collector thread typically runs at lower priority, so weak references might persist slightly longer than expected in practice. Nonetheless, when a GC cycle runs, weak references are always cleared.
Like soft references, weak references support reference queues. When a weak reference is cleared, the reference is added to the queue, allowing applications to track when objects have been collected.
Weak references are ideal for implementing caches or mappings that should not prevent collection. When memory is abundant, cached entries remain available; when memory is tight, they can be garbage collected without causing OOM errors.
JDK 1.2 introduced WeakReference to implement weak references.
Object strongRef = new Object();
WeakReference<Object> weakRef = new WeakReference<Object>(strongRef);
strongRef = null;
A key difference between soft and weak references is how the garbage collector handles them. Soft references require checking conditions before collection, while weak references are always collected during any GC pass.
public class WeakReferenceDemo {
public static class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
WeakReference<Person> personRef = new WeakReference<>(new Person(2, "janedoe"));
System.out.println("Initial: " + personRef.get());
System.gc();
System.out.println("After GC: " + personRef.get());
}
}
Interview question: Have you used WeakHashMap? Examining the WeakHashMap source code reveals that its Entry class extends WeakReference, which enables automatic cleanup when keys are no longer strongly referenced elsewhere.
Phantom References: Object Lifecycle Tracking
Phantom references are the weakest reference type available in Java. Unlike other reference types, phantom references do not prevent collection in any way—the referenced object behaves as if it has no references at all.
You cannot use phantom references to access the target object; calling get() always returns null. This reference type serves a single purpose: receiving notifications when an object is about to be garbage collected.
Phantom references must be used with a ReferenceQueue. When creating a phantom reference, you provide a reference queue as a constructor parameter. When the garbage collector prepares to collect an object that has a phantom reference, it adds the reference to the queue after collection. This allows applications to track which objects have been collected.
This capability is useful for performing cleanup operations that need to occur exactly once when an object is collected. Since phantom references are added to the queue only after the object has been finalized, they provide a reliable notification mechanism.
JDK 1.2 introduced PhantomReference for this purpose.
Object target = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(target, refQueue);
target = null;
Test implementation:
public class PhantomReferenceDemo {
public static PhantomReferenceDemo instance;
static ReferenceQueue<PhantomReferenceDemo> referenceQueue;
public static class QueueMonitor extends Thread {
@Override
public void run() {
while (true) {
if (referenceQueue != null) {
PhantomReference<PhantomReferenceDemo> ref = null;
try {
ref = (PhantomReference<PhantomReferenceDemo>) referenceQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ref != null) {
System.out.println("Object collected. Phantom reference detected in queue.");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize method invoked");
instance = this;
}
public static void main(String[] args) {
Thread monitor = new QueueMonitor();
monitor.setDaemon(true);
monitor.start();
referenceQueue = new ReferenceQueue<>();
instance = new PhantomReferenceDemo();
PhantomReference<PhantomReferenceDemo> phantomRef =
new PhantomReference<>(instance, referenceQueue);
System.out.println(phantomRef.get());
instance = null;
System.gc();
Thread.sleep(1000);
if (instance == null) {
System.out.println("Object was collected");
} else {
System.out.println("Object survived via finalize");
}
System.out.println("Second GC cycle");
instance = null;
System.gc();
Thread.sleep(1000);
if (instance == null) {
System.out.println("Object was collected");
} else {
System.out.println("Object survived");
}
}
}
Sample output:
Finalize method invoked
Object survived via finalize
Second GC cycle
Object collected. Phantom reference detected in queue.
Object was collected
Finalizer References
Finalizer references are an internal JVM mechanism used to support the finalize() method. They enable the JVM to invoke user-defined cleanup code before actually reclaiming an object's memory.
You do not need to write code to use finalizer references directly. The JVM handles them internally alongside reference queues. When garbage collection occurs, finalizer references are added to a dedicated queue. A background Finalizer thread processes this queue, calling the finalize() method for each referenced object before the object becomes eligible for collection in a subsequent GC cycle.
This two-phase approach ensures that finalization happens exactly once per object before memory reclamation.