Garbage collection is often treated as something that arrived with Java, but its history goes back further. Lisp, created at MIT in 1960, was already using dynamic memory allocation and garbage collection. In the JVM, GC is the automatic memory-management layer that frees objects no longer in use so their space can be reused.
Which memory needs reclaiming?
The JVM runtime areas behave differently. The program counter, virtual machine stack, and native method stack are tied to threads: they are born and die with threads, and stack frames are pushed and popped as methods enter and exit. Their memory layout is deterministic once the class structure is known, so allocation and reclamation there are predictable. The Java heap and method area are different. Interface implementations and conditional branches mean the program's object graph is not known until runtime. The number and size of objects created are dynamic. The garbage collector's main job is managing allocation and reclamation in this dynamic area. Automatic GC reduces manual memory-management errors and helps avoid leaks, though it does not remove the need to understand object lifetimes.
When does collection happen?
The trigger depends on the collector. Common situations include:
- An object is no longer reachable from any active reference, making it eligible for collection.
- Available memory approaches a limit, causing pressure-driven collection.
- Code calls
System.gc(). This is only a suggestion; the JVM is not obliged to collect immediately. - The application is idle, and the collector can use that time to reclaim memory.
How does collection work?
At a high level, a collector identifies live objects, finds the rest, reclaims their space, and sometimes compacts memory. The exact sequence varies by algorithm: marking live objects, scanning for unmarked objects, reclaiming them, and moving survivors to create contiguous free space.
Determining liveness
Reference counting
Reference counting keeps a counter on each object for the number of references to it. A new reference increments the counter; a removed reference decrements it. When the counter reaches zero, the object is dead. Collectors can periodically scan for zero-count objects and reclaim them. The obvious weakness is cycles: if two or more objects refer to each other, their counters never reach zero, so they can leak.
Reachability analysis
Reachability analysis is more common. It starts from a set of GC Roots and traverses references. Objects reached from the roots are marked live; unmarked objects are unreachable and can be reclaimed. This handles cycles because a cycle with no path from a root remains unmarked. Modern JVMs generally use reachability analysis.
The four reference types
Liveness is inseparable from references. Before JDK 1.2, an object was simply referenced or not. JDK 1.2 expanded this into four strengths:
- Strong reference: the ordinary reference. An object reachable through a strong reference is not collected even under memory pressure; it becomes collectible only when the strong reference is explicitly cleared.
- Soft reference: weaker than strong. The collector tries to keep soft-referenced objects, reclaiming them only when memory is genuinely low. Soft references are often used for memory-sensitive caches.
SoftReferencewas added in JDK 1.2. - Weak reference: weaker still. If an object is only weakly reachable, the collector may clear it at the next collection. Weak references are used in caches, observer patterns, and similar structures.
WeakReferencewas added in JDK 1.2. - Phantom reference: the weakest form. It cannot be used to retrieve the object. Its role is to track when an object has been collected, often for managing direct memory.
PhantomReferencewas added in JDK 1.2.
The following example demonstrates the behavior:
public class TestReference {
public static void main(String[] args) {
// Create a strong reference object
Object hardReference = new Object();
// Create a soft reference object
SoftReference<Object> softReference = new SoftReference<>(new Object());
// Create a weak reference object
WeakReference<Object> weakReference = new WeakReference<>(new Object());
// Create a weak reference object and assign its referent to a strong reference variable
WeakReference<Object> weakUseReference = new WeakReference<>(new Object());
Object hardUseReference = weakUseReference.get();
// WeakReference<Object> weakUseReference = new WeakReference<>(hardReference);
// Object hardUseReference = weakUseReference;
// Create a phantom reference object and specify a reference queue
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), referenceQueue);
// Trigger garbage collection
System.gc();
// Print the state of each reference object
System.out.println("HardReference Obj = " + hardReference);
System.out.println("SoftReference Obj = " + softReference.get());
System.out.println("WeakReference Obj = " + weakReference.get());
System.out.println("HardUseReference Obj = " + hardUseReference);
System.out.println("WeakUseReference Obj = " + weakUseReference.get());
System.out.println("PhantomReference Obj = " + phantomReference.get());
}
}
The strong reference should survive. The soft reference usually survives while memory is sufficient. The weak reference created with new Object() and held only weakly can be cleared. weakUseReference is different because the referent is also held strongly by hardUseReference after get(), so it remains reachable. In the commented variant, hardReference itself is the strong path to the referent. The important point is not whether the WeakReference wrapper is strongly held, but whether the referent has a strong path from a GC Root. A phantom reference cannot return its referent through get(), so it prints null; its queue is used to learn when the referent is enqueued.
One last rescue attempt
If an object has no reference chain from GC Roots, it is unreachable and theoretically reclaimable. The finalization mechanism can still give it one chance:
- First mark: unreachable objects are marked and filtered to decide whether
finalize()needs to run. finalize()execution: objects that need it are placed in theF-Queueand executed by theFinalizerthread. The JVM does not promise to wait for completion.- Second mark: the collector later processes the queue again. If an object reattaches itself to a reachable object inside
finalize(), it is removed from the 'about to be collected' set. - Reclamation: if the object does not rescue itself, it is collected.
The classic example:
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("I feel gooooooooooood :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize(); // Call the parent finalize() method
FinalizeEscapeGC.SAVE_HOOK = this; // Reconnect
System.out.println("finalize() method executed!");
}
public static void main(String[] args) throws Throwable {
// Create object link start!
SAVE_HOOK = new FinalizeEscapeGC();
for (int i = 0; i < 5; i++) {
// Break the link
SAVE_HOOK = null;
System.out.println("God ! Please! no ! Please do something! Save me!");
System.gc();
// Finalizer has low priority; pause for 0.5 seconds to wait for it
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("Wasted :(");
}
}
}
}
A typical run looks like this:
God ! Please! no ! Please do something! Save me!
finalize() method executed!
I feel gooooooooooood :)
God ! Please! no ! Please do something! Save me!
Wasted :(
God ! Please! no ! Please do something! Save me!
Wasted :(
God ! Please! no ! Please do something! Save me!
Wasted :(
God ! Please! no ! Please do something! Save me!
Wasted :(
The object escapes only the first collection. The JVM calls an object's finalize() automatically at most once. If the object faces collection again, finalize() will not run, so the later rescue attempts fail.
Garbage collection in practice
Generational collection theory
Generational collection rests on two empirical hypotheses:
- Weak generational hypothesis: most objects die young. They become garbage shortly after allocation, so a large share of objects have short lifetimes.
- Strong generational hypothesis: objects that survive multiple collections become increasingly hard to collect. Survivors tend to be long-lived structures such as core data structures or global state.
Under these assumptions, the heap is split into young and old generations so each can use a suitable policy. But there is a complication: during a Minor GC, if a young object is referenced by an old object, accurate liveness checking would require scanning the entire old generation to ensure reachability. That is expensive. A third hypothesis addresses it:
- Intergenerational reference hypothesis: cross-generation references are a small minority compared with same-generation references.
One corollary is that two objects with a reference relationship tend to live or die together. Still, the central idea is that the young generation produces garbage at a high rate, while the old generation produces it at a lower rate. Different collection strategies for different generations improve efficiency.
Marking algorithms
Three classic algorithms build on reachability and generational theory:
-
Mark-and-sweep: mark reachable objects from roots, then sweep the heap and reclaim unmarked objects. Its weaknesses are unstable efficiency and fragmentation; repeated runs leave discontinuous free memory, which can force another collection when a large object needs contiguous space.
-
Mark-and-copy: usually applied to the young generation. The young space is divided into two equal parts, and only one is used at a time. During collection, live objects are copied to the other part while the current part is cleared. This avoids fragmentation. It works well when most young objects die, but if most objects survive, copying overhead becomes large.
-
Mark-and-compact: usually applied to the old generation. It marks live objects like mark-and-sweep, but instead of simply sweeping, it moves live objects toward one end and clears the memory beyond the boundary. This keeps survivors contiguous and reduces fragmentation. The cost is moving live objects and updating references, so the algorithm is generally paired with old generations where garbage production is lower. If the old generation's object population makes compaction expensive, the moving work can become a heavy operation.
A mixed strategy sometimes avoids adding too much overhead to allocation and access: use mark-and-sweep most of the time and tolerate fragmentation until it starts to interfere with allocation. At that point, run a mark-and-compact collection to produce a more regular heap.
Classic collectors
Serial collector

Before JDK 1.3.1, this was the only young-generation collector in the JVM. It is single-threaded; when collection starts, all application threads are paused until collection finishes. It uses mark-copy for the young generation and mark-compact for the old generation. In client mode, typical desktop applications have modest heaps and can often finish collection quickly, so Serial remains the default young-generation collector for such environments.
ParNew collector

ParNew is essentially a multithreaded version of Serial. Apart from supporting parallel collection, its behavior matches Serial. Some server-mode JVMs use ParNew as the young-generation collector.
Parallel Scavenge / Parallel Old

Parallel Scavenge targets the young generation with mark-copy. Parallel Old targets the old generation with mark-compact. Unlike ParNew, this pair measures throughput and adapts collection timing to meet a throughput goal. That self-adaptive behavior can balance machine performance and choose a suitable collection plan. JDK8 uses Parallel Scavenge plus Parallel Old as its default configuration.
CMS collector

HotSpot introduced CMS (Concurrent-Mark-Sweep) in JDK 1.5, and it was a major step for strongly interactive applications: it was the first collector to run garbage-collection threads concurrently with user threads. It uses mark-sweep for the old generation and has these phases:
- Initial mark: stop the application, mark objects directly reachable from roots. This causes a short pause.
- Concurrent mark: run with application threads and traverse the object graph to mark reachable objects.
- Concurrent preclean: run concurrently and handle reference changes that occurred during concurrent marking.
- Final remark: stop the application and correct reference changes from the concurrent marking phase.
- Concurrent sweep: run concurrently and reclaim unmarked objects.
Mark-sweep leaves memory fragmented, which raises the chance of Full GC over time. Concurrent execution also consumes system resources and can slow user threads. CMS was still the best low-latency choice until G1 appeared.
Garbage First (G1) collector

G1 arrived in JDK7 as a server-oriented collector and replaced the JDK8 default Parallel Scavenge plus Parallel Old combination in JDK9. It uses a generational approach but divides the heap into equal-sized regions and adjusts them dynamically according to collection needs.

Its collection process is broadly similar to CMS:
- Initial mark: stop the application and mark objects directly reachable from roots. This causes a short pause.
- Concurrent mark: run with application threads and mark all reachable objects.
- Final remark: stop the application and correct reference changes from concurrent marking.
- Cleanup: run concurrently and reclaim unreferenced objects.
G1 collects regions independently, avoiding a full-heap scan and reducing pause time. Compared with CMS, it generally has better pause behavior and avoids fragmentation.
Memory allocation strategies
Common rules
JVM memory allocation decides how new objects receive space. Two common rules are:
- Objects are allocated in Eden first. The heap is divided into regions, and Eden is the main allocation area for new objects. When Eden fills, a Minor GC runs and surviving objects move to Survivor or the old generation.
- Large objects go directly to the old generation. If an object exceeds a threshold, the JVM may allocate it directly in the old generation. Large objects often have longer lifetimes, and direct allocation avoids repeated copying in the young generation.
Other allocation rules
- Long-lived objects promote to the old generation. Each object has an age counter in its header. An object born in Eden that survives a Minor GC moves to Survivor and its age increments. Each later Minor GC survival increments the age again. When the age reaches the threshold, default 15, the object moves to the old generation.
-XX:MaxTenuringThresholdsets this threshold. - Dynamic age determination. HotSpot does not always require an object to reach
-XX:MaxTenuringThreshold. If the total size of objects of the same age in Survivor exceeds half of Survivor space, objects at or above that age can promote immediately. This adapts to different programs and reduces collection frequency.
Space allocation guarantee
Before a Minor GC, the JVM checks whether the old generation has enough free space to hold all young-generation objects. If it does, the Minor GC is safe. If not, the JVM's behavior depends on settings. If HandlePromotionFailure is allowed, the JVM compares the old generation's free space with the average size of objects promoted in earlier collections. If free space is larger than that average, the JVM may attempt a risky Minor GC. The risk is that many young objects survive and need to be promoted directly into the old generation because Survivor cannot hold them. If the promotion requirement exceeds old free space, the guarantee fails. The JVM then typically runs a Full GC, which can cause a long pause. To avoid frequent Full GC, HandlePromotionFailure is often enabled so the JVM can try a risky Minor GC first. The comparison with the historical average is an estimate of how many objects will survive; if survivors are far above average, guarantee failure becomes more likely.