A starting point to debug a Java OutOfMemoryError
Tags: java, programming, tutorial
Published: 2023-01-17 17:04:34

A wise man once said, if you are a Java developer long enough you’ll eventually run into an OutOfMemoryError in production. This is the harsh fate that I met recently on a Spring Boot application I was working on.

I wrote this article to share a few of my learnings while investigating this error. I believe this article is a good starting point to help you debug your OutOfMemoryError.

So brace yourselves, fasten your seatbelts, and let’s dive right in.

**TL;DR**

- OutOfMemoryError is (basically) due to a lack of heap space
- Isolate the part of the code that triggers the issue using tools like MAT to analyse a heap dump, and either fix your code or increase the heap size
- Configure your JVM to make it deterministic and detect a potential OutOfMemoryError early

_This article was originally posted [here](https://blog.nimbleways.com/a-starting-point-to-debug-a-jvm-outofmemoryerror/)_

## The mighty JVM

To understand the root cause of the OutOfMemoryError, we first need to understand what the JVM is and how it is related to this error.

In the 1990s, well before the rise of docker and containerisation, Java delivered one promise : write once, run everywhere.

Java is a programming language that needs to be compiled. This is done by using the `javac` utility, which generates bytecode. bytecode is code that is neither machine code nor human readable code, but something in between.

The JVM (Java Virtual Machine)  is a piece of software that interprets this bytecode and turns it into machine code. Modern JVM implementations also offer AOT (Ahead Of Time) compilation capabilities to speed up the code execution. So although bytecode is not platform dependant, the JVM obviously is.

The JVM is also responsible for memory management.

**Resources to go further:**

- https://www.w3schools.in/java/java-virtual-machine
- https://www.geeksforgeeks.org/jvm-works-jvm-architecture/
- https://www.eclipse.org/openj9/docs/introduction/


## Java memory management

When you start a Java program, the JVM will reserve different memory areas for different purposes. The one that we are interested in is the heap area.


![Different JVM memory areas ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qbmvgnphse66f8wxxyao.png)


The heap is basically the part of the memory where the JVM will store the objects that you instantiate in your code. When an instantiated object is no longer needed, it is freed from memory by the garbage collector, which is managed by the JVM as well.

So when does an OutOfMemoryError occurs ? When you no longer have enough free space to store a new object in the heap.


![Bernie asking for memory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/znw7d8bol0i4o1c8fkzo.jpg)


**Resources to go further :**

- https://www.geeksforgeeks.org/java-memory-management/
- https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java

## OutOfMemoryError types

Well, I lied. I told you that the OutOfMemoryError is cause by lack of heap space (and that actually is one of the OutOfMemoryError types). But the truth is there are other causes that could trigger an OutOfMemoryError. In this [Oracle documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html) they list (and explain the possible cause of) 7 types of errors :

- java.lang.OutOfMemoryError: Java heap space
- java.lang.OutOfMemoryError: GC Overhead limit exceeded
- java.lang.OutOfMemoryError: Requested array size exceeds VM limit
- java.lang.OutOfMemoryError: Metaspace
- java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space?
- java.lang.OutOfMemoryError: Compressed class space
- java.lang.OutOfMemoryError: reason stack_trace_with_native_method

You can also have a look at this [repository](https://github.com/rangareddy/ranga-java-oom) with samples of code that trigger the different OutOfMemoryError types.

In my case I encountered the `GC Overhead limit exceeded` error. 

As per Oracle documentation :

> The detail message "GC Overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. [...] This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations.

Let's take a code sample that triggers this error, and see how to debug it.

## Debugging an OutOfMemoryError 🧐


![When I do Java, I do OoME](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ib3qipbkjmigdkfkbaf2.jpg)


There are tools that can help you debug, from my research I found 3 interesting ones:

- [VisualVM](https://visualvm.github.io/) (free tool maintained by Oracle)
- [MAT](https://www.eclipse.org/mat/) (free open source software maintained by Eclipse Foundation)
- [Jprofiler](https://www.ej-technologies.com/products/jprofiler/overview.html) (paid software)

We are going to demonstrate the first two tools using the `GCOverheadLimitDemo` from the the [repository mentioned above](https://github.com/rangareddy/ranga-java-oom). Let's say we want to generate 1.000.000 Employee objects and insert them into a database. We want to avoid inserting each object separately into the DB as each request is costly, and want instead to insert a list of Employee objects. Here is the corresponding code :

```java
// GCOverheadLimitDemo.java
// JVM Parameters: -Xms10m -Xmx10m -XX:+UseParallelGC
// We limit the JVM Heap memory to 10MB

package com.ranga.java.oom;

import java.util.ArrayList;
import java.util.List;

public class GCOverheadLimitDemo {
    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
				
        // I added this line here to give me time to configure VisualVM
				// To monitor the JVM before the end of code execution
				Thread.sleep(5000);
				
        for(int count=0; count<1000000; count++) {
            System.out.println(++count);
            long id = count;
            String name = "Ranga " + id;
            int age = count > 100 ? count % 100 : count;
            float salary = count * 0.05f;
            Employee employee = new Employee(id, name, age, salary);
            list.add(employee);
        }
        
        // Insert Employees into database here
    }
}

```

Below is the result of the execution of the above code:


![OoME is triggered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18jolhlnvjmsc8dzod0x.png)


## Monitoring with VisualVM

We are going to use VisualVM to monitor heap memory usage as well as the garbage collector activity.

In order to do it we need to execute our code and tell VisualVM to monitor the corresponding JVM (pretty straightforward). Below is the result:


![VisualVM dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3uy2d19er92kefax1q2e.png)


We can see that right before the OutOfMemoryError was triggered, the GC activity peaked. Excessive GC activity triggered the `GC Overhead limit exceeded` error and caused the code to crash.

Next we need to isolate the part of the code that triggered this error.

## Heap dump analysis with MAT

On top of monitoring the JVM activity, you can also generate a heap dump when an OutOfMemoryError is triggered. A heap dump is basically a snapshot of the heap, it contains all the objects that were stored in the heap at the moment the dump was generated. The goal is to analyse further the data constituting the heap dump to get a hint on the actual code that triggers the OutOfMemoryError. In order to achieve this I added the following JVM parameter `-XX:+HeapDumpOnOutOfMemoryError`.

When the OOM exception occurs, it generates a `.hprof` file. This file can be analysed using MAT. Note that before the heap is generated, a GC cycle is triggered and therefore the dump only contains live objects.

Below is the result for  `GCOverheadLimitDemo` example:

![MAT analysis of a .hprof file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79kwkpaxzkmdvpbj6cjk.png)


We can clearly see that the object that is taking most of heap space is an ArrayList of Employee objects. I can now look for places in my code where I instantiate such objects and either fix the part of the code responsible for the error (if relevant) or just increase the heap maximum size.

In our case for example, if we don't want or can't increase the maximum heap size, we can insert Employee objects by batch of 10.000 instead of 1.000.000:

```java
package com.ranga.java.oom;

import java.util.ArrayList;
import java.util.List;

public class GCOverheadLimitDemo {
    public static void main(String[] args) {
    
        List<Employee> list;
       	
        for(int i=0; i < 100; i++){
        	
            list = new ArrayList();
            
            // At the end of each loop, the initial list can be
            // Garbage collected, thus releasing memory
        	
            for(int count=0; count<10000; count++) {
                long id = count;
                String name = "Ranga " + id;
                int age = count > 100 ? count % 100 : count;
                float salary = count * 0.05f;
                Employee employee = new Employee(id, name, age, salary);
                list.add(employee);
            }
            
            // Insert Employees into database here
        }
    }
}

```

## Other tools to debug your OutOfMemoryError

There are some other tools worth mentioning to monitor / help debug OOM exceptions:

- [spring-boot-actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html) if you are running a Spring Boot backend (free)
- [Java flight recorder](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/performissues.html) (JFR) to register a JVM activity (free for non production usage only)

## A deterministic JVM

One important thing to consider before pushing your java application into production is your JVM settings. There are a lot of resources that list the most important parameters to configure, like [this article](https://www.baeldung.com/jvm-parameters) or [Oracle documentation](https://docs.oracle.com/en/java/javase/19/gctuning/ergonomics.html#GUID-DB4CAE94-2041-4A16-90EC-6AE3D91EC1F1). I highly recommend to check it out before reading further.

**TLDR;** it is considered best practice to at least configure: 

- The heap memory size using `-Xms` (minimum heap size) and `-Xmx` (maximum heap size) 
- The garbage collector algorithm like `-XX:+UseParallelGC` to use the parallel garbage collector algorithm for example

Let's say you have an `app.jar`  that you want to execute, you would use for example : `java -Xms10m -Xmx1G -XX:+UseParallelGC -j api.jar`.

## Tips and tricks

Ideally one would detect an OutOfMemoryError way before the code is pushed to production. So here are a few tips to help you do that:

- Configure your JVM to use as little heap memory as possible, this will help you detect quickly the memory and / or performance bottlenecks starting from your local environment
- Be “iso production” in all your environments (including local environment), this means using in all your environments:
  - The same JVM / JDK versions
  - The same configuration for the JVM
  - The same volume of data (if for example you use a big database in production, use in other environments a database with equivalent amount of data)
- Load test your code to simulate a production load (you can use tools like [k6](https://k6.io/) or [Gatling](https://gatling.io/))

## Conclusion 👋

So we explored a few ways to debug an OutOfMemoryError when using Java code, as well as some tips to detect early a potential OutOfMemoryError. I wrote this article after facing myself the same situation in production within a Spring Boot backend. But obviously when working with such backends there are other performance challenges to tackle, like the infamous N+1 query problem. You can check [this article](https://blog.nimbleways.com/using-entitygraphs-to-solve-n-1-query-problem/) if you want to learn how to tackle N+1 query issues within a Spring Boot backend.

Keep Javaing ☕️