Java Garbage Collection
How the JVM reclaims unused memory automatically through garbage collection.
Java Garbage Collection
In Java you never call free(). The JVM tracks every object you allocate and, when an object can no longer be reached by your running program, the garbage collector (GC) reclaims its memory automatically. You write code that creates objects; the GC quietly cleans up behind you. Understanding how it decides what is garbage — and where in the heap it looks — is the difference between code that scales and code that stalls under load.
Reachability and GC roots
The GC does not look for objects you are "done with". It looks for objects that are still reachable. Starting from a set of GC roots, it follows every reference. Anything it can reach is live; everything else is garbage, regardless of whether you think you still need it.
| GC root | Example |
|---|---|
| Local variables | A reference on a running thread's stack |
| Static fields | static final Logger LOG = ... |
| Active threads | A live Thread object |
| JNI references | Objects held by native code |
Setting a reference to null (or letting it fall out of scope) does not delete anything — it just removes one path to the object. The object becomes collectable only when no path from any root remains.
Object a = new Object(); // reachable via local variable 'a'
Object b = a; // now two references point to the same object
a = null; // still reachable through 'b' — not garbage
b = null; // now unreachable — eligible for collectionThe generational heap
Most objects die young — a request scope, a loop temporary, an intermediate string. The JVM exploits this weak generational hypothesis by splitting the heap into regions and collecting the young area far more often than the old one.
| Region | Holds | Collected |
|---|---|---|
| Young (Eden + 2 Survivor spaces) | Freshly allocated objects | Frequently, by a fast minor GC |
| Old (Tenured) | Objects that survived several minor GCs | Rarely, by a slower major/full GC |
| Metaspace | Class metadata (not your objects) | When classloaders are unloaded |
New objects land in Eden. A minor GC copies the few survivors into a Survivor space; objects that keep surviving are eventually promoted to the old generation. Because minor GCs only scan the small young region, they are cheap — which is why short-lived allocation in Java is fast.
Mark, sweep, compact
A collection runs in phases. First it marks every reachable object by walking the graph from the roots. Then it sweeps, freeing the unmarked objects. Many collectors add a compact phase that slides surviving objects together so free space is one contiguous block — which keeps allocation a simple pointer bump and prevents fragmentation.
// Pseudocode of what the collector does for you:
// 1. mark: visit(roots); for each reachable object, set live = true
// 2. sweep: for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update referencesYou can suggest a collection with System.gc(), but it is only a hint — the JVM may ignore it. Never rely on it for correctness; treat it as a diagnostic tool, not a memory-management strategy.
Reference strength
Not every reference keeps an object alive equally. The java.lang.ref package lets you tell the GC how badly you want an object kept, which is the basis of memory-sensitive caches.
| Reference | GC behaviour |
|---|---|
Strong (ordinary =) | Never collected while reachable |
SoftReference | Collected only when memory is low — good for caches |
WeakReference | Collected at the next GC once no strong refs remain |
PhantomReference | Used to schedule cleanup after collection |
import java.lang.ref.WeakReference;
byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null; // drop the only strong reference
// After the next GC, ref.get() may return null.Choosing a collector
The HotSpot JVM ships several collectors with different trade-offs between throughput (total work done) and latency (length of pauses). You pick one with a JVM flag; the default since Java 9 is G1.
| Collector | Flag | Best for |
|---|---|---|
| G1 (default) | -XX:+UseG1GC | Balanced latency/throughput, large heaps |
| Parallel | -XX:+UseParallelGC | Batch jobs that prize raw throughput |
| ZGC | -XX:+UseZGC | Very large heaps, sub-millisecond pauses |
| Serial | -XX:+UseSerialGC | Small heaps, single-core or containers |
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp
# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyAppA worked example
The program below makes the GC's behaviour observable. It pins one object with a strong reference, holds another only through a WeakReference, churns out a wave of short-lived garbage, then asks for a collection and reports what survived and how heap usage changed.
What to take from the run:
- The weak referent prints
truebefore the collection andfalseafter it, proving aWeakReferencedoes not keep its object alive once no strong reference remains. - The strongly-held
keptarray printssurvived: trueeven afterSystem.gc(), because it is reachable from a GC root and the collector must preserve it. - Roughly 300 MB of garbage is allocated (
Bytes allocated as garbage: 307200000), yet used heap only climbs to about 5 MB — minor GCs reclaim the short-lived loop arrays as fast as they are created. Runtime.maxMemory()reports the heap ceiling (about 256 MB here), set by-Xmx, whiletotalMemory() - freeMemory()is the live used portion that hovers near 3–5 MB throughout.System.gc()is only a hint, but on this JVM it does run: used heap drops back down and the unreachable weak referent is cleared rather than lingering.
Practice
An object is referenced only by a local variable that has just gone out of scope, and by nothing else. What makes it eligible for garbage collection?