Stage 8: Concurrency and the JVM, lesson 6 of 7

JVM memory and garbage collection

Advanced3 min readall versions
Explain it forThe essentials plus production detail and pitfalls.

The main JVM memory areas:

  • Heap: every object. Split into a young generation (Eden and survivor spaces) and an old generation.
  • Stack, one per thread: method frames, local variables and references.
  • Metaspace (Java 8+, replacing PermGen): class metadata, stored in native memory.
  • Code cache: native code produced by the JIT.

An object becomes garbage when no chain of references from a GC root (stack variables, static fields, active threads) reaches it. Most objects die young, so collecting the young generation often is cheap.

Collectors:

  • G1: the default since Java 9, and in every environment since Java 27. Balanced.
  • ZGC: sub-millisecond pauses on huge heaps; generational by default since Java 23.
  • Shenandoah: low pauses; its generational mode became production-ready in Java 25.
  • Parallel for maximum batch throughput, Serial for tiny heaps.

Stack, heap and metaspace

Each thread has a stack of frames holding local variables, primitives and references. Objects live on the shared heap. Class metadata lives in metaspace. Too-deep recursion overflows the stack (StackOverflowError); too many live objects fill the heap (OutOfMemoryError).

How garbage collection works

An object is garbage when nothing reachable from the GC roots (thread stacks, static fields and a few others) points to it. Most objects die young, so the heap is split into a young generation (collected often and cheaply) and an old generation (collected less often).

Choosing a garbage collector

  • G1 (the default): balanced throughput and pause times for most apps.
  • ZGC: very short pauses even with huge heaps; generational since Java 21 and the only mode since Java 23.
  • Parallel: maximum throughput for batch jobs, with longer pauses.
  • Serial: tiny heaps and single-CPU containers.

Pick one with flags such as -XX:+UseZGC.

Memory leaks in Java

Java still leaks when objects stay reachable by mistake: ever-growing static maps or caches without eviction, listeners that are never removed, ThreadLocal values in pools, and unclosed resources. Find them with a heap dump (jcmd <pid> GC.heap_dump) opened in Eclipse MAT or VisualVM.

OutOfMemoryError messages

  • "Java heap space": too many live objects, or the heap is too small.
  • "Metaspace": too many classes loaded (often a class-loader leak).
  • "GC overhead limit exceeded": the JVM spends almost all its time collecting.
  • "unable to create native thread": too many platform threads (virtual threads help).

Add -XX:+HeapDumpOnOutOfMemoryError in production so you have evidence.

Strong, soft, weak and phantom references

Normal references are strong. A SoftReference is cleared only when memory runs low (simple caches). A WeakReference is cleared at the next GC (WeakHashMap). Phantom references and Cleaner run clean-up after an object is gone, replacing finalize.

Heap sizing and containers

-Xms and -Xmx set the starting and maximum heap. In containers, the JVM reads the memory limit automatically; -XX:MaxRAMPercentage=75 is a common way to size the heap relative to it. Turn on GC logging (-Xlog:gc) before you tune anything.

Example

Terminal
# Size the heap and choose a collector
java -Xms512m -Xmx2g -XX:+UseZGC -jar app.jar

# In containers, size the heap relative to the memory limit
java -XX:MaxRAMPercentage=75 -jar app.jar

# Diagnose a running JVM
jcmd <pid> GC.heap_info
jcmd <pid> Thread.print                   # thread dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof   # open in Eclipse MAT
java -XX:StartFlightRecording=duration=60s,filename=rec.jfr -jar app.jar

Common mistake

Setting -Xmx equal to the container's memory limit. Metaspace, thread stacks and direct buffers live outside the heap, so the container gets killed. Leave headroom, for example MaxRAMPercentage=75.

Under the hood

Memory leaks in Java are references you forgot about: static collections that only grow, caches with no eviction, listeners never removed, ThreadLocal values left in pooled threads. Compact object headers (production-ready in Java 25, on by default in Java 27) shrink each object header from 12 to 8 bytes on typical 64-bit JVMs, which noticeably reduces heap use for object-heavy apps.

Check yourself

What replaced PermGen in Java 8?

How this connects

Part of Crack the Java interview, Upgrade from Java 8 to Java 25.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.