What's Actually Happening
Java application runs out of heap memory. JVM throws OutOfMemoryError, application crashes or becomes unstable.
The Error You'll See
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceApplication log:
java.lang.OutOfMemoryError: Java heap space
at com.example.MyClass.process(MyClass.java:42)JVM error:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid12345.hprof ...
Heap dump file created [123456789 bytes in 1.234 secs]GC overhead:
java.lang.OutOfMemoryError: GC overhead limit exceededWhy This Happens
- 1.Heap too small - JVM heap size insufficient for workload
- 2.Memory leak - Objects not being garbage collected
- 3.Large objects - Processing large data without streaming
- 4.Too many threads - Each thread uses stack memory
- 5.Inefficient code - Creating too many objects
- 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
| Check | Command | Expected |
|---|---|---|
| Heap size | jcmd VM.flags | Xmx set appropriately |
| Memory usage | jstat -gc | < 80% utilized |
| GC pauses | GC logs | No long pauses |
| Objects | jmap -histo | No unexpected large objects |
| Metaspace | jstat -gc | Within limit |
| Memory leak | heap dump analysis | No 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 ```
Related Issues
- [Fix Java StackOverflowError](/articles/fix-java-stackoverflowerror)
- [Fix Java ClassNotFoundException](/articles/fix-java-classnotfoundexception)
- [Fix Java NullPointerException](/articles/fix-java-nullpointerexception)