Command Palette

Search for a command to run...

Level 2 · 20 min

JVM Memory

Understanding how the JVM manages memory is essential for diagnosing OutOfMemoryErrors, tuning GC performance, and architecting systems that handle millions of objects efficiently. The JVM memory model divides memory into distinct regions, each with different lifetimes and management strategies.

JVM Memory Areas

The JVM memory is divided into: Heap (Young + Old generations for object allocation), Stack (per-thread, stores stack frames with local variables and partial results), Metaspace (class metadata, method bytecode, since Java 8 replaces PermGen — grows dynamically, bounded by -XX:MaxMetaspaceSize), Code Cache (JIT-compiled native code, bounded by -XX:ReservedCodeCacheSize), and off-heap areas like Direct Buffers (ByteBuffer.allocateDirect()). Each region has distinct failure modes: heap → OutOfMemoryError: Java heap space, metaspace → OutOfMemoryError: Metaspace.

Young and Old Generation, TLAB

The Young generation (Eden + two Survivor spaces S0/S1) is where all new objects are allocated. Eden is partitioned into Thread-Local Allocation Buffers (TLABs) — each thread gets a private chunk of Eden, allowing allocation without synchronization (just a pointer bump). Objects that survive a configurable number of minor GCs (-XX:MaxTenuringThreshold, default 15) are promoted to the Old generation. The Old generation is larger and collected less frequently. A minor GC collects only Young; a major/full GC collects everything. A critical insight from Java Concurrency in Practice: 'immutable objects offer additional performance advantages such as reduced need for locking or defensive copies and reduced impact on generational garbage collection.' This is because immutable objects can be freely shared between generations without write barriers — the GC's write barrier tracks Old-to-Young references (card tables), and mutable long-lived objects that reference short-lived ones incur write barrier overhead on every field mutation. TLABs (Thread-Local Allocation Buffers) are typically 1% of Eden size — on a 256 MB Eden, each thread gets roughly 256 KB of private allocation space, making object allocation nearly free (a pointer bump) at the cost of slightly increased Eden waste when threads die with partially-filled TLABs.

Class Loading

The JVM loads classes lazily on first use through the class loader hierarchy: Bootstrap → Extension/Platform → Application. Each class loader maintains its own namespace — two classes loaded by different loaders are distinct even if identical. Class loading is the source of ClassCastException across loader boundaries and metaspace leaks in dynamic environments (OSGi, hot-reload, application servers). When a class loader is GC-eligible (no live references), its loaded classes are unloaded and metaspace is freed.

Key Takeaways

  • New objects land in Eden (Young generation). TLAB makes allocation nearly free — just a pointer increment per thread.
  • Minor GC triggers when Eden fills up. Only Young generation is collected — typically pauses < 50ms for well-tuned heaps.
  • Metaspace grows unboundedly unless capped with -XX:MaxMetaspaceSize. Dynamic class loading (frameworks, scripting) can exhaust it.

Code example

// Checking JVM memory regions at runtime
Runtime rt = Runtime.getRuntime();
long heapUsed = rt.totalMemory() - rt.freeMemory();
long heapMax = rt.maxMemory();

// Force TLAB flush + minor GC (diagnostic only — never in prod)
System.gc();

// Key JVM flags for memory tuning
// -Xms512m -Xmx2g          # initial and max heap
// -XX:NewRatio=2            # Old:Young = 2:1
// -XX:MaxMetaspaceSize=256m # cap metaspace
// -XX:+PrintGCDetails       # verbose GC log (Java 8)
// -Xlog:gc*                 # unified logging (Java 9+)