Java · Topic 1 of 8

JVM memory and garbage collection

Card 1 of 14

What you will learn

The JVM splits memory into a shared heap, per-thread stacks, and native-memory Metaspace, and the garbage collector reclaims heap objects that are no longer reachable. Knowing how generations, G1, and ZGC behave helps you read GC logs, size containers, and find leaks.

Where memory lives

The heap is shared by all threads and holds objects and arrays. Each thread has its own stack with one frame per method call, holding locals and references. Metaspace holds class metadata and lives in native memory, not in the heap.

Off-heap memory also counts toward process size: thread stacks, direct buffers, Metaspace, and the code cache. Setting -Xmx equal to the container limit is therefore a mistake.

Example

Where memory lives

java -Xms512m -Xmx2g -Xss512k \
     -XX:MaxMetaspaceSize=256m App

Reachability and generations

The GC does not count references. It traces from GC roots such as thread stacks and static fields, and anything not reached is garbage, even in a cycle.

Most objects die young, so collectors split work into young and old data. New objects start in Eden, and each thread allocates from its own TLAB, a private chunk that avoids locking. Survivors are copied and eventually promoted. The JIT can also skip allocating objects that never escape a method, using escape analysis.

Choosing a collector

G1 is the default since Java 9. It splits the heap into regions, collects the most garbage-heavy ones first, and aims for a soft pause goal set by -XX:MaxGCPauseMillis. Large objects become humongous allocations that use contiguous regions.

ZGC targets very low pauses with concurrent work. In Java 25, -XX:+UseZGC runs generational ZGC, the only mode. Parallel favors throughput and Serial suits tiny heaps.

Example

Choosing a collector

-XX:+UseG1GC          # default
-XX:+UseZGC           # generational ZGC
-XX:+UseParallelGC    # throughput

Reading GC logs

Use unified logging to see what the collector is doing. A line shows the pause type, heap before and after, and the pause time.

Watch for Full GC events, frequent young pauses, and G1 messages like To-space exhausted, which mean it ran out of room to copy survivors. Long or growing old-generation usage after each collection often points to a leak.

Example

Reading GC logs

java -Xlog:gc*:file=gc.log:time,uptime App
# [2.1s] GC(7) Pause Young (Normal) 512M->96M(1024M) 8.4ms

Leaks and OutOfMemoryError

A Java leak is an object that stays reachable but is never used again. Typical causes are static caches without eviction, listeners never removed, ThreadLocal values in pooled threads, and class loaders kept after redeploys.

The error message names the area: Java heap space, Metaspace, GC overhead limit exceeded, or Direct buffer memory. Capture evidence automatically and inspect the dump for what retains the most memory.

Example

Leaks and OutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/dumps

References and compact headers

Strong references keep objects alive. SoftReference is cleared under memory pressure, WeakReference at the next collection once only weakly reachable, and PhantomReference never exposes its referent and is used for cleanup through Cleaner. Finalizers are deprecated for removal, so avoid them.

Compact object headers are final in Java 25 but opt-in with -XX:+UseCompactObjectHeaders. They shrink the header to 8 bytes on 64-bit and can reduce heap use.

Key points

  • Heap is shared and collected; each thread has its own stack; Metaspace holds class metadata in native memory.
  • GC finds garbage by tracing from roots, so unreachable cycles are collected.
  • G1 is the default; -XX:+UseZGC runs generational ZGC in Java 25.
  • MaxGCPauseMillis is a soft goal, and System.gc() is only a hint.
  • Process memory is larger than -Xmx because of off-heap areas.
  • Use -Xlog:gc and heap dumps to diagnose pauses and leaks.

Common mistakes

  • Setting -Xmx equal to the container memory limit and ignoring off-heap usage.
  • Treating MaxGCPauseMillis as a guarantee or calling System.gc() to fix performance.
  • Caching in a static Map with no eviction or size bound.
  • Forgetting ThreadLocal.remove() in thread pools.
  • Relying on finalize instead of try-with-resources or Cleaner.

Lesson complete

Ready to check what you learned?

Swipe left or right, or use the buttons.