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 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. (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 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.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
staticcollection (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
HashMaplong 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.
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.
| 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.