Playing with Java Out Of Memory Exception

Playing with Java Out Of Memory Exception

I was recently trying to simulate java.lang.OutOfMemoryError in my local. This was to learn and also understand how to deal with them in case they happen in our Togai production environment

My idea was to run a code that takes up a lot of memory in the heap. And I wanted to do this in a controlled/sandboxed environment because I didn't want to use up all the 16GB memory in my work laptop 💻 using a Java program to just see java.lang.OutOfMemoryError exception in my local

Controlled/sandboxed environment - container? VM (Virtual Machine)? micro VM? I chose container because that's too easy and quick to spin up, some VMs and micro VMs are easy and quick to spin up too by the way

Now, I know where to run the Java code or Kotlin or any code for that matter, that can be compiled and run in JVM (Java Virtual Machine). I have Docker installed on my machine. Now I just need the code that can take up lots of memory in the heap. I did a quick Google search and found a nice little program here - https://mkyong.com/java/how-to-simulate-java-lang-outofmemoryerror-in-java/ by
Yong Mook Kim

// JavaEatMemory.java
import java.util.ArrayList;
import java.util.List;

public class JavaEatMemory {

    public static void main(String[] args) {

            List<byte[]> list = new ArrayList<>();
            int index = 1;
            while (true) {
                    // 1MB each loop, 1 x 1024 x 1024 = 1048576
                    byte[] b = new byte[1048576];
                    list.add(b);
                    Runtime rt = Runtime.getRuntime();
                    System.out.printf("[%d] free memory: %s%n", index++, rt.freeMemory());
            }

    }
}

I saved this in a file named JavaEatMemory.java

Next, I wrote a Dockerfile to build the code and run it

FROM eclipse-temurin:11 AS builder
WORKDIR /app
COPY JavaEatMemory.java /app
RUN javac JavaEatMemory.java

FROM eclipse-temurin:11
WORKDIR /app
COPY --from=builder /app/JavaEatMemory.class /app
CMD ["java", "-Xmx5000K", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/app/heap-dumps", "JavaEatMemory"]

As you can see I built (compiled) the code inside the Docker container itself using the multi-stage build feature, so if you are trying this, you don't even need Java (Java Development Kit, Java Runtime Environment etc) installed on your machine

I have used eclipse-temurin JDK distribution and used Java v11 because that's the one we are running in our Togai production environment. You can choose any JDK distribution and any Java version. Note that I have used some extra options in the java command and options differ/are based on the JDK distribution from what I read here

Let me explain some of the options I have used here

Xmx - I used this to set a max size for the heap. Proper documentation can be found by using Java Oracle docs / whatever docs of distribution you use, or man java / man java | cat which shows the below content

       -Xmxsize
           Specifies the maximum size (in bytes) of the memory allocation pool in bytes. This value must be a
           multiple of 1024 and greater than 2 MB. Append the letter k or K to indicate kilobytes, m or M to
           indicate megabytes, g or G to indicate gigabytes. The default value is chosen at runtime based on system
           configuration. For server deployments, -Xms and -Xmx are often set to the same value. See the section
           "Ergonomics" in Java SE HotSpot Virtual Machine Garbage Collection Tuning Guide at
           http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html.

           The following examples show how to set the maximum allowed size of allocated memory to 80 MB using
           various units:

               -Xmx83886080
               -Xmx81920k
               -Xmx80m

           The -Xmx option is equivalent to -XX:MaxHeapSize.

-XX:+HeapDumpOnOutOfMemory - as the name suggests - was used to take a dump of the heap when java.lang.OutOfMemoryError error occurs

-XX:HeapDumpPath - as the name suggests - when taking a dump of the heap, the path to store the dump can be mentioned using this option

Again, these options can be found using man java or man java | cat

       -XX:+HeapDumpOnOutOfMemory
           Enables the dumping of the Java heap to a file in the current directory by using the heap profiler
           (HPROF) when a java.lang.OutOfMemoryError exception is thrown. You can explicitly set the heap dump file
           path and name using the -XX:HeapDumpPath option. By default, this option is disabled and the heap is not
           dumped when an OutOfMemoryError exception is thrown.

       -XX:HeapDumpPath=path
           Sets the path and file name for writing the heap dump provided by the heap profiler (HPROF) when the
           -XX:+HeapDumpOnOutOfMemoryError option is set. By default, the file is created in the current working
           directory, and it is named java_pidpid.hprof where pid is the identifier of the process that caused the
           error. The following example shows how to set the default file explicitly (%p represents the current
           process identificator):

               -XX:HeapDumpPath=./java_pid%p.hprof

           The following example shows how to set the heap dump file to /var/log/java/java_heapdump.hprof:

               -XX:HeapDumpPath=/var/log/java/java_heapdump.hprof

By the way, I wanted to use these options in production so I wanted a way to test them locally first by using the same JDK distribution and version as the one in production. So, this was another reason to try it in my local to test if these options will work whenever java.lang.OutOfMemoryError error happens in our Togai production environment. We can't test in production now, can we? :P

Anyways, now we have a Dockerfile, we can build it using docker CLI command and the Docker Engine (Docker daemon) running behind the scenes

docker build -t out-of-memory-jar .

This will build the Docker container image we require. Let's now run the Docker container image

mkdir heap-dumps # to store the heap dumps

docker run --rm --memory 10MB -v $(pwd)/heap-dumps:/app/heap-dumps  out-of-memory-jar

You should get an output like this -

$ docker run --rm --memory 10MB -v $(pwd)/heap-dumps:/app/heap-dumps  out-of-memory-jar
[1] free memory: 4507776
[2] free memory: 3352800
[3] free memory: 2304320
[4] free memory: 1255872
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /app/heap-dumps/java_pid1.hprof ...
Heap dump file created [6784892 bytes in 0.131 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at JavaEatMemory.main(JavaEatMemory.java:12)

And you should be able to see the heap dump in the heap-dumps directory

$ ls -lh heap-dumps 
total 13256
-rw-------@ 1 karuppiah  staff   6.5M Jul 25 20:46 java_pid1.hprof

That's all, go on and experiment with this code and different options to try anything related to Java out-of-memory errors or other Java stuff too. Maybe create a blog post for a similar experiment but for another programming language like Golang, Ruby, Crystal, Ziglang, Erlang, Elixir, Haskell etc :D or for Java itself ;) or something like Kotlin, Clojure etc languages that run on JVM

Some extra experiments that I did in the above setup

Set a very low value for the maximum heap size

I set -Xmx as 500K, like this -

FROM eclipse-temurin:11 AS builder
WORKDIR /app
COPY JavaEatMemory.java /app
RUN javac JavaEatMemory.java

FROM eclipse-temurin:11
WORKDIR /app
COPY --from=builder /app/JavaEatMemory.class /app
CMD ["java", "-Xmx500K", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/app/heap-dumps", "JavaEatMemory"]

When I built the Docker container image and ran the container, the output was like this -

$ docker build -t out-of-memory-jar .
$ docker run --rm --memory 10MB out-of-memory-jar
Error occurred during initialization of VM
Too small maximum heap

$ echo $?
1

As you can see, JVM throws an error saying that the maximum heap size is too small

The JVM kept throwing the same error when I tried to increase the maximum heap size to say 600K, or even 1000K and then even 2000K

Finally, I noticed that the JVM does not throw an error when the maximum heap size is set to 2048K which is 2MB when you consider 1MB as 1024KB. So I guess for this JVM version and distribution, the minimum value of the maximum heap size is 2048K

Set a very low value for the Docker container memory limit

When I tried a value like 5MB for the Docker container running my notorious Java program that eats memory, this is the output I got -

$ docker run --rm --memory 5MB out-of-memory-jar
docker: Error response from daemon: Minimum memory limit allowed is 6MB.
See 'docker run --help'.

Looks like the minimum memory limit that I can set with this version of Docker Engine / Daemon is 6MB

Set a higher value for the maximum heap size than the Docker container memory limit

For example, I set 15000K (around 15MB) as the maximum heap size

FROM eclipse-temurin:11 AS builder
WORKDIR /app
COPY JavaEatMemory.java /app
RUN javac JavaEatMemory.java

FROM eclipse-temurin:11
WORKDIR /app
COPY --from=builder /app/JavaEatMemory.class /app
CMD ["java", "-Xmx15000K", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/app/heap-dumps", "JavaEatMemory"]

And then I built the Docker container image and ran the Docker container with 10MB memory limit - which is lower than the maximum heap size we just set

$ docker build -t out-of-memory-jar .;

And the output I saw was this -

$ docker run --rm --memory 10MB out-of-memory-jar
[1] free memory: 6366272
[2] free memory: 5371968
[3] free memory: 4323376
[4] free memory: 3402888
[5] free memory: 2354296
[6] free memory: 7237408
[7] free memory: 6188816
[8] free memory: 5140224

$ echo $?
137

We can see the 137 exit code here, which means the process was killed with a SIGKILL signal. I guess the operating system used the out-of-memory killer (OOM Killer) to kill the process taking up too much memory

This shows us that it's important to tell the JVM a valid maximum heap size - something less than the total memory available in the environment (e.g. container, physical machine, VM etc) and not something more than the total memory available. Even when I gave an equal memory limit for the container and the JVM maximum heap size, the process got killed

For you to get the java.lang.OutOfMemoryError error and a heap dump when it happens, to check for any issues in heap, it's important to set a valid value for the maximum heap size