Memory Management in Java: Beyond the Basics

Memory Management in Java: Beyond the Basics

Java’s automatic memory management (garbage collection) frees developers from manually allocating and freeing memory, but understanding how it works is essential for writing high-performance applications. A Java process that runs out of memory or spends too much time in GC pauses can bring down a service. This article explores the JVM heap structure, garbage collection algorithms, JVM flags for tuning, and tools for diagnosing memory issues.

Heap Structure and Generations

The JVM heap is divided into regions based on the age of objects. The Young Generation is where new objects are allocated. It is further divided into Eden (where most objects are initially allocated) and two Survivor spaces (S0 and S1). Most objects die young — studies show that 90-95% of objects become unreachable within a few milliseconds. These are collected by minor GC, which is fast and pauses the application briefly. Objects that survive multiple minor GC cycles are promoted to the Old Generation (also called the Tenured Generation), which holds long-lived objects. The Metaspace (replacing the old PermGen in Java 8+) stores class metadata and is not part of the heap.

# Common JVM heap sizing flags
-Xms4g          # Initial heap size (4 GB)
-Xmx4g          # Maximum heap size (4 GB)
-XX:NewRatio=2  # Old:Young ratio (2:1 — 2/3 old, 1/3 young)
-Xmn1g          # Explicit young generation size (1 GB)
-XX:SurvivorRatio=8  # Eden:Survivor ratio (8:1:1)

# View heap defaults for your JVM version
java -XX:+PrintFlagsFinal -version | grep -E 'HeapSize|NewSize|SurvivorRatio'

Choosing the right heap size is a tradeoff. A heap that is too small causes frequent GC cycles and potential OutOfMemoryErrors. A heap that is too large increases GC pause times (the JVM has more memory to scan for live objects) and makes tuning harder. A good starting point is -Xms4g -Xmx4g (equal initial and max to avoid resizing overhead) and adjust based on monitoring. The NewRatio determines the proportion of young vs old generation — for applications with high allocation rates (web servers, batch processors), a larger young generation reduces minor GC frequency.

Garbage Collection Algorithms

The JVM offers several GC implementations, each optimized for different workloads. G1 GC (Garbage First) has been the default since Java 9. It divides the heap into 1 MB regions and prioritizes collecting regions with the most garbage first. G1 is designed for heaps up to 100 GB and targets low pause times with the MaxGCPauseMillis flag. ZGC (Java 15+) is a concurrent garbage collector that keeps pause times under 1 millisecond regardless of heap size, making it ideal for latency-sensitive applications. Shenandoah (Java 15+, experimental in earlier versions) is another low-pause collector that performs compaction concurrently with the application threads.

# G1 GC (default since Java 9)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200     # target max pause time
-XX:G1HeapRegionSize=4M      # region size (1-32 MB)
-XX:G1NewSizePercent=5       # initial young gen as % of heap
-XX:G1MaxNewSizePercent=60   # max young gen as % of heap

# ZGC (ultra-low latency)
-XX:+UseZGC
-Xmx16g                       # ZGC works best with large heaps
-XX:ZAllocationSpikeTolerance=2.0  # handle allocation spikes

# Shenandoah
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive  # compact, static, or aggressive

# Enable GC logging for analysis (Java 17+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+age=trace

Detecting and Fixing Memory Leaks

A Java memory leak occurs when objects that are no longer needed are still referenced by live objects, preventing garbage collection. Common causes include: forgetting to close resources (input streams, database connections, HTTP clients — which is why try-with-resources is critical), registering listeners or callbacks without deregistering them, static collections that grow unbounded, ThreadLocal variables that are not cleaned up, and custom class loaders that are never garbage collected. Tools for detecting leaks include heap dump analysis with Eclipse MAT or JProfiler, the jmap command-line tool, and the jconsole monitoring tool.

# Take a heap dump (use jmap)
jmap -dump:format=b,file=heap.hprof <pid>

# Take a heap dump automatically on OutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

# Analyze heap with jhat (basic, included with JDK)
jhat heap.hprof

# Count instances of a class (live objects)
jmap -histo:live <pid> | head -20

# Monitor GC activity
jstat -gcutil <pid> 1000    # poll every 1 second

# Using jconsole or VisualVM for GUI monitoring
jconsole <pid>

# Common leak pattern: unbounded static collection
public class Cache {
    private static final Map<String, byte[]> store = new HashMap<>();
    // Without eviction, this grows indefinitely — use WeakHashMap or
    // a bounded cache like Caffeine or Guava Cache
}

Memory management in Java is not set-and-forget. Monitor GC frequency, pause times, and heap usage in production. Use GC logs to correlate pause times with application latency. Right-size the heap based on actual usage, not assumptions. And always enable HeapDumpOnOutOfMemoryError in production — the heap dump is the most valuable diagnostic tool when something goes wrong.

Leave a Reply

Your email address will not be published. Required fields are marked *