JVM memory: stack, heap, and garbage collection
Where locals and objects live, why Java is always pass-by-value, how a tracing collector decides what is garbage, and what a Java memory leak really is.
After this lesson you can
- Say what lives on a thread's stack and what lives on the heap
- Explain why reassigning a parameter never affects the caller
- Describe reachability from GC roots and the generational heap
- Explain what retained size means when hunting a leak
Every thread has its own stack: one frame per method call in progress, holding that call's parameters and local variables. A local of a primitive type holds the value itself; a local of a reference type holds a reference — the object it points to lives on the heap, which all threads share. (The JIT compiler may avoid allocating an object that never escapes a method, but that is an optimisation you cannot observe.) Class metadata lives outside the heap, in metaspace.
Always pass-by-value
Java passes every argument by value. For a reference, the value that is copied is the reference, so a method can change the object both copies point at, but reassigning its own parameter only changes its own copy.
import java.util.*; public class Solution { static void addItem(List<String> list) { list.add("added"); // changes the shared object } static void replace(List<String> list) { list = new ArrayList<>(List.of("replaced")); // changes only the local copy list.add("more"); } static void increment(int n) { n++; // a copy of the value } public static String describe() { List<String> items = new ArrayList<>(List.of("start")); addItem(items); replace(items); int count = 1; increment(count); return "items=" + items + " count=" + count; }}A stack is small (typically up to about 1 MB per platform thread by
default, set with -Xss), so unbounded recursion ends in
StackOverflowError. Running out of heap ends in OutOfMemoryError.
Both are Errors, not Exceptions: you can catch them, but rarely
should.
public class Solution { static int depth = 0; static void recurse() { depth++; recurse(); // no base case } public static String describe() { depth = 0; try { recurse(); return "returned normally"; } catch (StackOverflowError e) { boolean deep = depth > 1000; return "caught " + e.getClass().getSimpleName() + ", an Error: " + (e instanceof Error) + ", an Exception: " + (((Throwable) e) instanceof Exception) + ", more than 1000 frames: " + deep; } }}Garbage collection is about reachability
Java's collectors are tracing collectors. They start from the GC roots — local variables in live stack frames, static fields, active threads, JNI references — and mark everything reachable by following references. Anything not marked is garbage, however many references it holds to itself or to other garbage: a cycle of objects that nothing reachable points to is collected like anything else.
Most objects die young, so the heap is generational. New objects
are allocated in the young generation (Eden); a frequent, cheap minor
GC copies the few survivors out and empties Eden. Objects that survive
enough minor collections are promoted to the old generation, which is
collected less often. Since Java 9 the default collector is G1,
which splits the heap into regions and aims at a pause-time target —
but only on a machine the JVM considers server-class (at least two
processors and roughly 2 GB of memory). In a smaller container the JVM
picks the single-threaded Serial collector instead, which is one
reason to set the collector explicitly in production. ZGC offers very
short pauses for large heaps (its generational mode was added in Java
21 behind -XX:+ZGenerational).
What a leak means in Java
A Java memory leak is an object that is still reachable but will
never be used again: entries added to a static map used as a cache
and never evicted, listeners registered and never removed, a
ThreadLocal on a pooled thread. The collector cannot free them —
they are not garbage by its definition. In a heap dump, the number that
points at a leak is retained size: the memory that would become
unreachable, and so collectable, if that one object went away.
Common mistakes
- Writing a
swap(a, b)method for two object references and expecting the caller's variables to swap. Only the copies swap. - Calling to "free memory". It is a hint the JVM may ignore, and it can trigger an expensive full collection.
Exercise · Pro
Retained size in a tree of references
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade