Phase 4 · Advanced JavaModule 27~44 min read

JVM Internals & Memory Management

Look under the hood: class loading, the stack and heap, garbage collection, and the JIT.

What you'll learn

You've been running on the JVM this whole time — now let's look under the hood. Understanding how the JVM loads classes, manages memory, and collects garbage helps you write efficient code and diagnose the tough bugs: leaks, out-of-memory errors, and performance mysteries.

By the end you'll be able to:

  • Describe the JVM's architecture and class loading
  • Explain the difference between the stack and the heap
  • Understand garbage collection and reference types
  • Recognise the causes of memory leaks, OutOfMemoryError, and StackOverflowError
  • Understand what the JIT compiler does

JVM architecture & class loading

The JVM is the engine that runs your bytecode (Module 1). When your program references a class for the first time, a class loader finds its .class file, verifies the bytecode is safe, and loads it into memory — lazily, only when needed. Loaders form a hierarchy (bootstrap → platform → application), each delegating upward before trying itself.

Class metadata lives in a region called Metaspace (outside the heap). The two memory areas you'll think about most, though, are the stack and the heap.

Stack vs heap

This distinction explains a huge amount of Java's behaviour — including pass-by-value (Module 6) and why objects are shared. Every thread has its own stack for method calls and local variables; all threads share one heap where objects live:

Two kinds of memory

The Stack

  • • One per thread
  • • Holds method call frames
  • • Local variables & primitives
  • • Object references (the arrows)
  • • Fast; freed automatically when a method returns

The Heap

  • • Shared by all threads
  • • Holds every object & array
  • • Where new allocates
  • • Cleaned by the garbage collector
  • • Larger and longer-lived
Memory.java
void demo() {
    int count = 5;                 // primitive -> lives on the STACK
    String name = "Sara";          // reference on stack, String on HEAP
    int[] data = new int[1000];    // reference on stack, array on HEAP
}
// When demo() returns, the stack frame (count, name, data) is gone.
// The objects on the heap stay until no references point to them.

Key idea

Primitives and references sit on the stack; the objects they point to sit on the heap. When a method returns, its stack frame vanishes instantly — but heap objects live on until the garbage collector decides no one needs them.

Garbage collection

Unlike C, you never manually free memory in Java. The garbage collector (GC) automatically reclaims heap objects that are no longer reachable — nothing references them anymore. It works on a simple insight: most objects die young, so the heap is split into young and old generations that are collected at different rates.

Note

Modern JVMs ship several collectors (G1 is the default; ZGC and Shenandoah offer very low pause times). You rarely pick one for small apps — but for high-performance systems, choosing and tuning the GC is a real skill (Module 38).

Reference types & memory leaks

The GC decides what to keep based on how objects are referenced. Beyond ordinary (strong) references, Java offers weaker kinds for caches and cleanup:

ReferenceGC behaviourUse for
StrongNever collected while reachablenormal references (the default)
SoftCollected only when memory is lowmemory-sensitive caches
WeakCollected at the next GCcanonical maps, listeners
PhantomFor pre-cleanup actionsadvanced resource management

"Automatic" memory doesn't mean "leak-proof." A memory leak in Java happens when you keep references to objects you no longer need — so the GC can't reclaim them. The usual culprit is a long-lived collection that only grows:

Leak.java
import java.util.*;

// A classic leak: a static collection that only ever grows.
class Cache {
    static final List<byte[]> HELD = new ArrayList<>();

    static void add() {
        HELD.add(new byte[1_000_000]);   // never removed -> heap fills up
    }
}

Memory errors

  • OutOfMemoryError — the heap is full and the GC can't free enough (often a leak, or too small a heap)
  • StackOverflowError — the call stack is exhausted, almost always from unbounded recursion
Overflow.java
static int boom(int n) {
    return boom(n + 1);   // no base case -> stack frames pile up
}
// Calling boom(0) throws StackOverflowError

Watch out

Both are Errors, not exceptions — you don't catch them, you fix the cause: remove the leak, add a recursion base case, or right-size the heap with the -Xmx flag.

JIT compilation

The JVM starts by interpreting bytecode, but it watches which methods run often ("hot" code). The Just-In-Time (JIT) compiler then compiles those hot paths to native machine code and optimises them aggressively — which is why Java reaches near-C speed after a brief warm-up. This is also why micro-benchmarks are tricky (Module 38): the first runs are slow, then the JIT kicks in.

Recap & quick check

Key takeaways

  • Class loaders lazily load, verify, and link classes; metadata lives in Metaspace.
  • The stack holds method frames, locals, and references (one per thread); the heap holds objects (shared).
  • Garbage collection automatically frees unreachable heap objects — no manual free().
  • Leaks come from keeping references you no longer need; StackOverflowError from unbounded recursion.
  • The JIT compiles hot methods to native code after a warm-up, giving Java its speed.

Quick check

1. Where are objects created with 'new' stored?

2. What does the garbage collector reclaim?

3. What usually causes a StackOverflowError?

4. Can Java programs still have memory leaks?

5. What does the JIT compiler do?

Excellent — you now understand what happens beneath your code. Next up: Module 28 — Modern Java Language Features, the finale of Phase 4.