Java is a robust and memory-managed programming language, but like any managed environment, it’s still susceptible to memory issues. One of the most dreaded runtime problems in a Java application is the java.lang.OutOfMemory error. This error is thrown when the Java Virtual Machine (JVM) cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.

This article walks through the common causes of OutOfMemoryError, how to detect and diagnose them, and most importantly, how to fix them with practical coding examples and memory management strategies.

Understanding What Causes OutOfMemoryError

There are several types of OutOfMemoryErrors in Java, including:

  • Java heap space

  • GC overhead limit exceeded

  • Metaspace

  • Direct buffer memory

  • Unable to create new native thread

Let’s break these down one by one:

1. java.lang.OutOfMemoryError: Java heap space

This is the most common form. It occurs when the JVM heap memory is exhausted and no more memory is available for object allocation.

Example Code:

java
import java.util.ArrayList;
import java.util.List;
public class HeapMemoryLeak {
public static void main(String[] args) {
List<int[]> memoryHog = new ArrayList<>();
while (true) {
memoryHog.add(new int[1_000_000]); // Each array is ~4MB
}
}
}

Fixes:

  • Increase Heap Size: Modify JVM options:

    bash
    java -Xms512m -Xmx2g HeapMemoryLeak
  • Profile Your Application: Use tools like VisualVM or Eclipse MAT to detect memory leaks or large object retention.

  • Code Fix: Avoid unbounded collections.

    java
    if (memoryHog.size() > 100) {
    memoryHog.clear();
    }

2. java.lang.OutOfMemoryError: GC overhead limit exceeded

This error indicates that the JVM is spending too much time performing garbage collection and has recovered very little memory.

Code to Reproduce:

java
import java.util.HashMap;
import java.util.Map;
public class GCOverheadTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; ; i++) {
map.put(Integer.toString(i), String.valueOf(Math.random()));
}
}
}

Fixes:

  • Memory Leak Analysis: Use a memory profiler to find object references that are not being released.

  • Increase Heap or Tune GC:

    bash
    java -Xmx2g -XX:-UseGCOverheadLimit GCOverheadTest

    Be careful with disabling UseGCOverheadLimit, as it may mask deeper issues.

  • Implement Eviction Logic:

    java
    if (map.size() > 100_000) {
    map.clear();
    }

3. java.lang.OutOfMemoryError: Metaspace

From Java 8 onwards, class metadata is stored in Metaspace instead of PermGen. This error happens when too many classes are loaded dynamically.

Example Scenario: This commonly occurs in applications that use frameworks like Spring or Hibernate improperly, or when deploying multiple applications on the same JVM (like in a servlet container).

Fixes:

  • Increase Metaspace Size:

    bash
    java -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=128m YourApp
  • Unload Classes Properly: If using custom class loaders, ensure classes are correctly unloaded.

  • Inspect Class Loading: Tools like JVisualVM and jcmd can show loaded classes:

    bash
    jcmd <pid> GC.class_histogram

4. java.lang.OutOfMemoryError: Direct buffer memory

Occurs when using ByteBuffer.allocateDirect() and the allocated memory exceeds the -XX:MaxDirectMemorySize.

Example Code:

java

import java.nio.ByteBuffer;

public class DirectBufferTest {
public static void main(String[] args) {
while (true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024); // 10MB
}
}
}

Fixes:

  • Tune Direct Memory Size:

    bash
    java -XX:MaxDirectMemorySize=256m DirectBufferTest
  • Reuse Buffers: Instead of creating new ones in loops, pool and reuse buffers.

5. java.lang.OutOfMemoryError: unable to create new native thread

This error typically occurs when the JVM reaches the OS thread limit. Each thread consumes native memory, and too many threads can exhaust system resources.

Example Code:

java
public class ThreadExplosion {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
try {
Thread.sleep(1000000);
} catch (InterruptedException ignored) {}
}).start();
}
}
}

Fixes:

  • Thread Pooling: Use Executors.newFixedThreadPool() to limit thread creation.

    java
    ExecutorService pool = Executors.newFixedThreadPool(10);
  • Monitor OS Limits: On Unix:

    bash
    ulimit -u # Check user process limit
  • Use Lightweight Concurrency Models: Consider using asynchronous APIs or frameworks like Vert.x or Project Loom (virtual threads).

How to Diagnose OutOfMemoryErrors

  1. Heap Dumps:

    • Use -XX:+HeapDumpOnOutOfMemoryError to generate a .hprof file on crash.

      bash
      java -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof YourApp
  2. Monitoring Tools:

    • JConsole, VisualVM, JProfiler, and YourKit can visualize memory usage.

    • Java Flight Recorder (JFR) provides low-overhead production profiling.

  3. Logs and Error Messages:

    • Check logs for stack traces and memory pool exhaustion.

    • Use -verbose:gc and -Xlog:gc* to trace GC activity.

Best Practices to Prevent OutOfMemoryError

  • Limit Cache Size: Use LinkedHashMap with eviction policy or Guava Cache.

  • Use Weak/Soft References:

    java
    WeakReference<Object> weakRef = new WeakReference<>(new Object());
  • Avoid Large Object Allocation: Chunk large data processing instead of loading everything into memory.

  • Properly Close Resources: Unclosed streams and JDBC connections can hold references.

  • Deploy Application with Suitable JVM Options: Tune heap, stack size, Metaspace, and GC for your app’s specific needs.

Conclusion

OutOfMemoryErrors are often a sign of deeper issues within a Java application—whether it’s a memory leak, inefficient data structures, unbounded caching, thread mismanagement, or improper JVM configurations.

Understanding the different memory areas (heap, metaspace, direct memory, native threads) and how the JVM manages them is crucial to diagnosing the root causes. While increasing memory limits may seem like a quick fix, it’s usually more effective to profile and optimize the application’s memory usage.

By applying proactive techniques such as memory profiling, heap dump analysis, proper use of collections, thread management, and JVM tuning, developers can build applications that are not only stable under normal loads but also resilient under high stress.

Finally, consider integrating monitoring solutions (like Prometheus with Grafana, or APM tools like New Relic and AppDynamics) into production environments to catch memory trends early—before they bring down your application.

Java’s memory management is powerful, but not foolproof. With the right diagnostics and fixes, OutOfMemoryErrors can go from fatal runtime problems to manageable hiccups in your software development lifecycle.