W3docs

Java Garbage Collection

How Java garbage collection works: reachability and GC roots, the generational heap, mark-sweep-compact, reference types, and collectors.

In Java you never call free(). The JVM tracks every object you allocate on the heap 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.

This page covers how the GC decides what to keep (reachability), how the heap is laid out for fast collection, the mark-sweep-compact algorithm, the four reference strengths, how to pick a collector, and a runnable demo that makes collection observable.

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. (For how the stack and heap differ, see Stack vs Heap; for the role of the JVM that hosts the heap, see JVM Architecture.)

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.

Memory leaks still happen

A garbage collector frees you from dangling pointers and double frees, but not from leaks. A Java memory leak is an object you no longer use that is still reachable from a root, so the GC must keep it. The heap fills up, GC runs more and more often, and eventually you hit OutOfMemoryError.

The classic causes are all "I forgot to let go":

  • A static collection (cache, listener list, map) that you keep adding to but never remove from. Static fields are GC roots, so everything they reach lives forever.
  • Listeners or callbacks registered with a long-lived object and never unregistered.
  • Keys left in a HashMap long after they are needed, because the map still references them.

The fix is not a flag — it is to drop references when you are done (remove from the collection, unregister the listener) or to use a WeakReference-based structure such as WeakHashMap so the GC can reclaim entries once nothing else points at the key.

Warning
Java has no destructors, and finalize() is deprecated and unreliable — the JVM may run it late or not at all. To release files, sockets, or other non-memory resources deterministically, use try-with-resources and AutoCloseable, not the garbage collector.

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?
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?
Was this page helpful?