What's Actually Happening

Java application runs out of heap memory. JVM throws OutOfMemoryError, application crashes or becomes unstable.

The Error You'll See

bash
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Application log:

bash
java.lang.OutOfMemoryError: Java heap space
    at com.example.MyClass.process(MyClass.java:42)

JVM error:

bash
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid12345.hprof ...
Heap dump file created [123456789 bytes in 1.234 secs]

GC overhead:

bash
java.lang.OutOfMemoryError: GC overhead limit exceeded

Why This Happens

  1. 1.Heap too small - JVM heap size insufficient for workload
  2. 2.Memory leak - Objects not being garbage collected
  3. 3.Large objects - Processing large data without streaming
  4. 4.Too many threads - Each thread uses stack memory
  5. 5.Inefficient code - Creating too many objects
  6. 6.Caching without limits - Cache growing unbounded

Step 1: Check Current Heap Settings

```bash # Check running JVM options: jcmd <pid> VM.flags

# Check heap size: jcmd <pid> VM.info | grep -A5 "Heap"

# Or: jmap -heap <pid>

# Check memory usage: jstat -gc <pid>

# Output columns: # S0C, S1C: Survivor space capacity # S0U, S1U: Survivor space used # EC, EU: Eden space capacity/used # OC, OU: Old generation capacity/used # MC, MU: Metaspace capacity/used

# Check process memory: ps aux | grep java top -p <pid>

# Check environment: echo $JAVA_OPTS echo $MAVEN_OPTS ```

Step 2: Increase Heap Size

```bash # Set heap size with JVM options:

# Initial and max heap: java -Xms512m -Xmx2g MyApp

# Or in environment: export JAVA_OPTS="-Xms512m -Xmx2g"

# For application servers:

# Tomcat (setenv.sh): export CATALINA_OPTS="-Xms512m -Xmx2g"

# Spring Boot: java -jar -Xms512m -Xmx2g app.jar

# Maven build: export MAVEN_OPTS="-Xms512m -Xmx2g"

# Gradle build: export GRADLE_OPTS="-Xms512m -Xmx2g"

# Kubernetes deployment: env: - name: JAVA_OPTS value: "-Xms512m -Xmx2g"

# Docker: docker run -e JAVA_OPTS="-Xms512m -Xmx2g" myimage

# Rule of thumb: # - Set Xms = Xmx for predictable memory # - Max heap should be < 80% of container memory # - Leave room for Metaspace, thread stacks, native memory ```

Step 3: Choose Right Garbage Collector

```bash # Available GCs:

# Serial GC (small apps, single CPU): java -XX:+UseSerialGC -Xmx2g MyApp

# Parallel GC (throughput focused, default in JDK 8): java -XX:+UseParallelGC -Xmx2g MyApp

# G1GC (balanced, default in JDK 9+): java -XX:+UseG1GC -Xmx2g MyApp

# ZGC (low latency, JDK 15+): java -XX:+UseZGC -Xmx4g MyApp

# Shenandoah (low latency): java -XX:+UseShenandoahGC -Xmx4g MyApp

# G1GC tuning: -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # Target pause time -XX:InitiatingHeapOccupancyPercent=35 # Start GC earlier -XX:G1HeapRegionSize=16m # Region size

# Check GC logging: -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10m

# JDK 8 GC logging: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log ```

Step 4: Analyze Memory with Tools

```bash # Generate heap dump: jcmd <pid> GC.heap_dump /tmp/heap.hprof

# Or on OOM: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp MyApp

# Use jvisualvm: jvisualvm

# Use VisualVM: # Download from: https://visualvm.github.io/ # Open heap dump file

# Use Eclipse MAT: # Download from: https://eclipse.dev/mat/ # Open heap dump, run Leak Suspects report

# Use jmap histogram: jmap -histo <pid> | head -30

# Shows object counts and sizes

# Use jcmd: jcmd <pid> GC.class_histogram | head -30

# Check live objects only: jmap -histo:live <pid> | head -30 ```

Step 5: Fix Common Memory Leaks

```java // Common leak patterns:

// 1. Static collections: // BAD: Never cleared public class Cache { private static Map<String, Object> cache = new HashMap<>();

public static void put(String key, Object value) { cache.put(key, value); // Grows forever } }

// FIX: Use LRU cache or WeakHashMap private static Map<String, Object> cache = new LinkedHashMap<>(100, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 1000; // Limit size } };

// 2. Unclosed resources: // BAD: Connection conn = dataSource.getConnection(); // ... use connection // Forgot to close

// FIX: Use try-with-resources try (Connection conn = dataSource.getConnection()) { // ... use connection }

// 3. Listener not removed: // BAD: button.addActionListener(listener); // Never removed

// FIX: button.removeActionListener(listener);

// 4. Thread local not cleared: // BAD: private static ThreadLocal<BigObject> local = new ThreadLocal<>();

// FIX: Always remove try { local.set(bigObject); // use } finally { local.remove(); } ```

Step 6: Optimize Object Creation

```java // Reduce object creation:

// BAD: String concatenation in loop: String result = ""; for (Item item : items) { result += item.toString(); // Creates new String each iteration }

// FIX: Use StringBuilder: StringBuilder sb = new StringBuilder(); for (Item item : items) { sb.append(item.toString()); } String result = sb.toString();

// BAD: Creating objects in loop: for (int i = 0; i < 10000; i++) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // use sdf }

// FIX: Reuse objects: SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); for (int i = 0; i < 10000; i++) { // use sdf }

// Use object pools for expensive objects: private static final ObjectPool<ExpensiveObject> pool = new GenericObjectPool<>(new ExpensiveObjectFactory());

// Use primitives instead of wrappers: // BAD: List<Integer> list = new ArrayList<>(); // Integer objects // FIX: int[] array = new int[size]; // Primitive array ```

Step 7: Handle Large Data Processing

```java // Process large files with streaming:

// BAD: Load entire file into memory: List<String> lines = Files.readAllLines(path); // OOM for large files

// FIX: Stream line by line: try (Stream<String> lines = Files.lines(path)) { lines.forEach(line -> process(line)); }

// Process large datasets: // BAD: List<Order> orders = orderRepository.findAll(); // All in memory

// FIX: Pagination or streaming: Page<Order> page = orderRepository.findAll(PageRequest.of(0, 1000));

// Or use cursor/JDBC streaming: @Transactional public void processAll() { try (Stream<Order> stream = orderRepository.streamAll()) { stream.forEach(this::process); } }

// Batch processing: int batchSize = 1000; for (int offset = 0; ; offset += batchSize) { List<Order> batch = repository.findBatch(offset, batchSize); if (batch.isEmpty()) break; processBatch(batch); } ```

Step 8: Configure Metaspace

```bash # Metaspace stores class metadata:

# Default is unlimited, can grow until system memory exhausted

# Limit Metaspace: -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m

# If getting Metaspace OOM: java.lang.OutOfMemoryError: Metaspace

# Check Metaspace usage: jstat -gc <pid> | awk '{print $12, $13}' # MC, MU columns

# Increase limit: -XX:MaxMetaspaceSize=1g

# Class loading issues: # If many classes loaded dynamically (frameworks, proxies) # Might need more Metaspace or investigate class leaks ```

Step 9: Monitor Memory Usage

```bash # Enable JMX for monitoring: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false

# Connect with JConsole: jconsole <pid>

# Connect with VisualVM: jvisualvm --openjmx <pid>:9010

# Use Java Flight Recorder: -XX:StartFlightRecording=duration=60s,filename=recording.jfr

# Prometheus metrics: # Add Micrometer dependency # Expose /actuator/prometheus endpoint

# Monitor with custom code: Runtime runtime = Runtime.getRuntime(); long usedMemory = runtime.totalMemory() - runtime.freeMemory(); long maxMemory = runtime.maxMemory(); double usedPercent = (double) usedMemory / maxMemory * 100;

if (usedPercent > 80) { log.warn("Memory usage high: {}%", usedPercent); } ```

Step 10: Java Heap Verification Script

```bash # Create verification script: cat << 'EOF' > /usr/local/bin/check-java-heap.sh #!/bin/bash

PID=$1

if [ -z "$PID" ]; then echo "Usage: $0 <java-pid>" echo "Java processes:" jps -l exit 1 fi

echo "=== JVM Version ===" jcmd $PID VM.version

echo "" echo "=== JVM Flags ===" jcmd $PID VM.flags

echo "" echo "=== Memory Pools ===" jmap -heap $PID 2>/dev/null || echo "Cannot attach to process"

echo "" echo "=== GC Statistics ===" jstat -gc $PID

echo "" echo "=== Heap Histogram (top 20) ===" jcmd $PID GC.class_histogram 2>/dev/null | head -25

echo "" echo "=== Memory Usage ===" jstat -gcutil $PID

echo "" echo "=== System Memory ===" cat /proc/$PID/status | grep -E "VmSize|VmRSS|VmPeak|VmSwap"

echo "" echo "=== Recommendations ===" HEAP_USED=$(jstat -gc $PID | tail -1 | awk '{print $3+$5+$7}') echo "Check heap usage and adjust -Xmx if needed" echo "Generate heap dump: jcmd $PID GC.heap_dump /tmp/heap.hprof" echo "Analyze dump with VisualVM or Eclipse MAT" EOF

chmod +x /usr/local/bin/check-java-heap.sh

# Usage: /usr/local/bin/check-java-heap.sh <pid>

# Quick check: alias java-heap='jstat -gc' ```

Java Heap Checklist

CheckCommandExpected
Heap sizejcmd VM.flagsXmx set appropriately
Memory usagejstat -gc< 80% utilized
GC pausesGC logsNo long pauses
Objectsjmap -histoNo unexpected large objects
Metaspacejstat -gcWithin limit
Memory leakheap dump analysisNo growing objects

Verify the Fix

```bash # After fixing heap issues

# 1. Check heap size jcmd <pid> VM.flags | grep Xmx // Appropriate size set

# 2. Monitor usage jstat -gcutil <pid> 1000 // Usage stable, not growing

# 3. Check GC jstat -gc <pid> // Eden and Old space cycling

# 4. Run load test // No OutOfMemoryError

# 5. Monitor over time watch -n 10 'jstat -gcutil <pid>' // Usage pattern stable

# 6. Check logs grep OutOfMemory /var/log/app.log // No new errors ```

  • [Fix Java StackOverflowError](/articles/fix-java-stackoverflowerror)
  • [Fix Java ClassNotFoundException](/articles/fix-java-classnotfoundexception)
  • [Fix Java NullPointerException](/articles/fix-java-nullpointerexception)