W3docs

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 rootExample
Local variablesA reference on a running thread's stack
Static fieldsstatic final Logger LOG = ...
Active threadsA live Thread object
JNI referencesObjects 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 collection

The 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.

RegionHoldsCollected
Young (Eden + 2 Survivor spaces)Freshly allocated objectsFrequently, by a fast minor GC
Old (Tenured)Objects that survived several minor GCsRarely, by a slower major/full GC
MetaspaceClass 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 references

You 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.

ReferenceGC behaviour
Strong (ordinary =)Never collected while reachable
SoftReferenceCollected only when memory is low — good for caches
WeakReferenceCollected at the next GC once no strong refs remain
PhantomReferenceUsed 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.

CollectorFlagBest for
G1 (default)-XX:+UseG1GCBalanced latency/throughput, large heaps
Parallel-XX:+UseParallelGCBatch jobs that prize raw throughput
ZGC-XX:+UseZGCVery large heaps, sub-millisecond pauses
Serial-XX:+UseSerialGCSmall 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* MyApp

A 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.

java— editable, runs on the server

What to take from the run:

  • The weak referent prints true before the collection and false after it, proving a WeakReference does not keep its object alive once no strong reference remains.
  • The strongly-held kept array prints survived: true even after System.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, while totalMemory() - 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

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?