JVM Memory & Garbage Collection: What Java Developers Need to Know

Every Java interview eventually lands here: "heap vs stack?", "how does GC work?" Most answers are memorized definitions. This post gives you the mental model that makes the definitions obvious — where objects live, what the collector actually does, and the one rule that matters most: diagnose before you tune.

1. Heap vs stack vs metaspace: three neighborhoods

The JVM splits memory by lifetime and purpose, not by type:

  • Stack — one per thread. Holds local variables and call frames. Memory is freed the instant a method returns. Small, fast, and never garbage collected — a StackOverflowError means runaway recursion, not a leak.
  • Heap — one shared region. Every new object lands here. This is the only region the garbage collector manages. A OutOfMemoryError: Java heap space means the heap filled with objects that are still reachable.
  • Metaspace — class metadata (loaded classes, method bytecode). Grows as classes load; a leak here (OutOfMemoryError: Metaspace) usually means a classloader leak — common with hot redeploys, not with normal code.
public class MemoryHood {
    static String shared = "heap: one copy, shared by all threads";  // heap (static field)

    public static void main(String[] args) {
        int count = 3;                       // stack: dies when main() returns
        String greeting = new String("hi");  // stack holds the REFERENCE; "hi" lives on the HEAP
        helper(greeting);
    }

    static void helper(String s) {
        int len = s.length();  // stack: new frame, new locals, freed on return
        System.out.println(len + " " + shared);
    }
}
2 heap: one copy, shared by all threads
Stack (per thread) local variables call frames freed on return never GC'd Heap (shared) every new object young + old generations the GC's territory OutOfMemoryError lives here Metaspace class metadata bytecode leak = classloader leak

The decision rule: when you see an OutOfMemoryError, the suffix tells you which neighborhood to investigate — Java heap space (reachable objects piling up), Metaspace (classloader leak), Unable to create new native thread (thread/stack exhaustion, not the heap at all).

2. How garbage collection works: the generational bet

The GC's entire design rests on one observed fact — the generational hypothesis: most objects die young. A request-scoped DTO lives for milliseconds; a cached config lives for hours. Collecting them the same way would be wasteful, so the heap is split:

  • Young generation — where everything is born. Collected often (minor GC); most objects are already dead, so it's cheap.
  • Old generation — survivors graduate here after enough young collections. Collected rarely (major/full GC); more expensive.

The collector's job, at a high level: find everything reachable from GC roots (thread stacks, statics), and reclaim the rest. "Stop-the-world" pauses are the price — the application threads freeze while the collector works. Modern collectors (G1, the default since Java 9; ZGC/Shenandoah for sub-millisecond pause targets) shrink those pauses by doing most work concurrently, but the fundamental tradeoff — throughput vs pause time — never disappears.

Watch it happen. Run any allocation-heavy program with GC logging and read the story:

public class GcStory {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            byte[] garbage = new byte[10 * 1024 * 1024];  // 10MB of "request data"
            System.out.println("allocated chunk " + i + ", len=" + garbage.length);
        }
    }
}
// Run: java -Xlog:gc*:file=gc.log GcStory

The log shows a repeating rhythm: allocation in Eden (young gen), a quick minor GC reclaims the dead chunks in milliseconds, and almost nothing ever reaches the old generation. That's the generational hypothesis working as designed — and it's why a healthy application's GC log is boring.

3. The one rule: diagnose before you tune

The most expensive GC mistake is tuning flags before measuring. The defaults (G1, ergonomically sized heap) are right for the overwhelming majority of applications. Tune only when a measured problem points at the collector:

  • Frequent long pauses correlating with latency spikes → look at pause times in the GC log before touching flags.
  • Heap keeps growing after full GCs → that's a leak (or an undersized heap), not a tuning problem — take a heap dump.
  • Someone suggests -XX:+UseParallelGC "for speed" → ask what the pause-time requirement is first. Throughput collectors trade longer pauses for total throughput; for a latency-sensitive API that's the wrong trade.

When you do need to look deeper — thread dumps, heap dumps, and JFR — that's the next post's territory: production diagnosis.

4. The interview one-liners

  • Stack = per-thread locals and frames, freed on return, never GC'd. Heap = all objects, the GC's territory. Metaspace = class metadata; a leak there means a classloader leak.
  • Generational GC bets most objects die young: cheap frequent minor collections in the young gen, rare expensive collections in the old gen.
  • G1 is the default; ZGC/Shenandoah target sub-millisecond pauses. Throughput vs pause time is the eternal tradeoff.
  • Read the OutOfMemoryError suffix — it names the neighborhood. And diagnose before you tune: the defaults are usually right.

Continue: Java Learning Roadmap 2026

Comments

Popular posts from this blog

JSP Servlet Interview Questions For Freshers Series 1

Java Banking Finance Services and Insurance (BFSI) domain interview questions

Java program to check even or odd number