How to Prevent java.lang.OutOfMemoryError in Java

Encountering a java.lang.OutOfMemoryError can be frustrating, but we can take several practical steps to prevent it. In this article, we explain what causes this error, provide real world examples, and discuss effective techniques to manage memory in our Java applications.

Table of contents

Understanding java.lang.OutOfMemoryError

A java.lang.OutOfMemoryError occurs when the Java Virtual Machine (JVM) cannot allocate enough memory for an object. This error often happens when:

  • The heap size is too small for the workload.
  • We have a memory leak where objects that are no longer needed remain referenced.
  • We allocate too many objects without releasing them.

Common Causes and Prevention Techniques

1. Optimize Heap Size

One straightforward way to prevent an OutOfMemoryError is by configuring the heap size properly. We can increase the maximum heap size using JVM options. For example:


java -Xms512m -Xmx1024m -jar our-application.jar

In this command, -Xms sets the initial heap size, and -Xmx sets the maximum heap size. Adjusting these parameters helps ensure that our application has enough memory during peak loads.

2. Identify and Fix Memory Leaks

Memory leaks occur when we unintentionally keep references to objects that are no longer needed. We can use tools like VisualVM or JProfiler to monitor memory usage and identify leaks.

Real World Example with Try-With-Resources:

Imagine our application creates many database connections but never closes them. Over time, these unused connections accumulate and eventually exhaust available memory. To prevent this, we must ensure that every connection is closed properly after use. With Java 7 and above, we can use the try-with-resources statement to automatically close our connections:


try (Connection conn = dataSource.getConnection()) {
    // Use the connection to execute queries, e.g., prepare and execute a statement.
} catch (SQLException e) {
    e.printStackTrace();
}

Using try-with-resources ensures that our connection is closed automatically, even if an exception occurs, which helps us avoid memory leaks.

3. Use Efficient Data Structures and Algorithms

Sometimes, inefficient use of data structures can lead to excessive memory consumption. We can optimize memory usage by:

  • Choosing the right data structures (e.g., using an ArrayList instead of a LinkedList when random access is needed).
  • Avoiding holding on to large data sets longer than necessary.
  • Removing objects from collections when they are no longer needed.

Real World Example:

If we are processing large batches of data, we might use a loop to add items to a list without ever clearing it:


List<Data> dataList = new ArrayList<>();
while (processingData) {
    Data data = fetchData();
    dataList.add(data);
    // Process data...
}

If the data list grows indefinitely, it can trigger an OutOfMemoryError. Instead, we can process and clear data in batches:


List<Data> dataList = new ArrayList<>();
while (processingData) {
    Data data = fetchData();
    dataList.add(data);
    
    if (dataList.size() >= BATCH_SIZE) {
        processBatch(dataList);
        dataList.clear(); // Clear the list to free up memory
    }
}

This strategy helps us manage memory more efficiently by periodically releasing unneeded objects.

4. Tuning Garbage Collection

The JVM’s garbage collector plays a vital role in reclaiming memory. We can tune garbage collection to suit our application’s needs by choosing the right collector and adjusting its parameters. Below are several examples along with the Java versions that support each collector.

Garbage Collector Compatibility by Java Version

Garbage Collector Java Version Support
Serial GC Available in all Java versions. It is a simple, single-threaded collector, ideal for small applications.
G1 GC Introduced in Java 7 (update 4) and available in Java 8. It became the default collector for server-class machines starting with Java 9.
Concurrent Mark Sweep (CMS) GC Available in Java 6, 7, and 8. Note that CMS was deprecated in Java 9 and may be removed in future versions.
Z Garbage Collector (ZGC) Introduced as an experimental feature in Java 11 and officially supported from Java 15 onward, offering low latency and scalability.

Using G1 Garbage Collector with Verbose Logging

The G1 garbage collector is designed for applications that require large heaps with predictable pause times. We can enable it and log GC details with the following command:


java -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar our-application.jar

This command helps us monitor how the garbage collector is performing and can be useful for troubleshooting memory issues.

Using Concurrent Mark Sweep (CMS) Garbage Collector

For applications that require low pause times, the CMS garbage collector might be a good choice (for Java 6–8):


java -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -jar our-application.jar

Here, -XX:CMSInitiatingOccupancyFraction sets the occupancy threshold that triggers the CMS collector. This adjustment helps us control when garbage collection occurs, reducing the chance of an OutOfMemoryError.

Using Serial Garbage Collector

For small applications or environments with limited resources, the Serial Garbage Collector is a simple option available in all Java versions:


java -XX:+UseSerialGC -jar our-application.jar

The Serial GC performs garbage collection in a single thread, which can be sufficient for lightweight applications.

Using Z Garbage Collector (ZGC)

For applications running on Java 11 and above that require low latency and can handle larger heaps, we might experiment with the Z Garbage Collector:


java -XX:+UseZGC -jar our-application.jar

ZGC is designed for applications with high memory demands and minimizes pause times, making it a good candidate for modern, memory-intensive applications.

Conclusion

We have discussed several strategies to prevent java.lang.OutOfMemoryError in Java. By properly configuring the heap size, identifying and fixing memory leaks (with techniques like try-with-resources), using efficient data structures, and tuning garbage collection based on the Java version, we can build robust and reliable applications. Implementing these best practices helps us ensure that our applications run smoothly and efficiently.

References

mkyong

Founder of Mkyong.com, passionate Java and open-source technologies. If you enjoy my tutorials, consider making a donation to these charities.

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments