/docker-jvm-memory-test

A Docker image meant to experiment with the many ways a JVM application can leak memory.

Primary LanguageJavaMIT LicenseMIT

Introduction

This is a Docker image meant to experiment with Docker's resource constraints and the many ways a JVM application can leak memory.

First run

$ docker run -it valentinomiazzo/jvm-memory-test

It just prints the output of jcmd continuosly. Something like:

Native Memory Tracking:

Total: reserved=1348MB, committed=17MB
-                 Java Heap (reserved=64MB, committed=2MB)
                            (mmap: reserved=64MB, committed=2MB)

-                     Class (reserved=1032MB, committed=5MB)
                            (classes #414)
                            (mmap: reserved=1032MB, committed=5MB)

-                    Thread (reserved=6MB, committed=6MB)
                            (thread #11)
                            (stack: reserved=6MB, committed=6MB)

-                      Code (reserved=244MB, committed=3MB)
                            (mmap: reserved=244MB, committed=2MB)

-                    Symbol (reserved=1MB, committed=1MB)
                            (malloc=1MB #103)

In another shell you can also observer the point of view of Docker and compare it with the one of jcmd. The two are similar but not identical.

$ docker stats $(docker ps -l -q)

Something like:

CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
83c9a5545f48        0.18%               83.47 MB / 134.2 MB   62.19%              648 B / 648 B       0 B / 426 kB

Simulate a memory leak on the heap

In this case we tell to the container:

  • to leak 1MB of heap at every cycle (default period 1 second)
  • the JVM will give an OutOfMemoryException at 256MB
  • the container has max 64MB of RAM assigned ...
  • ... and 0MB of swap memory (memory-memory-swap == 0)
$ docker run -it --memory=64m --memory-swap=64m --env ALLOC_HEAP_MB=1 --env MAX_HEAP_SIZE_MB=256 valentinomiazzo/jvm-memory-test

If you leave the container run for some seconds you will see that the container will exit printing something like.

Native Memory Tracking:

Total: reserved=1540MB, committed=61MB
-                 Java Heap (reserved=256MB, committed=46MB)
                            (mmap: reserved=256MB, committed=46MB)

-                     Class (reserved=1032MB, committed=5MB)
                            (classes #414)
                            (mmap: reserved=1032MB, committed=5MB)

-                    Thread (reserved=6MB, committed=6MB)
                            (thread #11)
                            (stack: reserved=6MB, committed=6MB)

-                      Code (reserved=244MB, committed=3MB)
                            (mmap: reserved=244MB, committed=2MB)

-                        GC (reserved=1MB, committed=0MB)
                            (mmap: reserved=1MB, committed=0MB)

-                    Symbol (reserved=1MB, committed=1MB)
                            (malloc=1MB #103)

PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c java      -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -XX:-AutoShutdownNMT      -Xm
    6 root       0:00 java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -XX:-AutoShutdownNMT -Xmx256m -Xms1m -Xss256k
  318 root       0:00 ps 6
6:
java.io.IOException: Connection refused
	at sun.tools.attach.LinuxVirtualMachine.connect(Native Method)
	at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:124)
	at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
	at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
	at sun.tools.jcmd.JCmd.executeCommandForPid(JCmd.java:147)
	at sun.tools.jcmd.JCmd.main(JCmd.java:131)

This is what happened:

  • the GC of the JVM tried allocate another chunk of RAM for the Heap
  • the cgroup associated with the container went over 64MB of RAM
  • the kernel/docker killed the process of the JVM
  • the infinite loop calling jcmd was executed yet another time
  • the JVM process was not found by the jcmd loop and therefore the CMD completed
  • the Docker container exited.

You can check that Docker actually terminated the container with

$ docker inspect -f '{{json .State}}' $(docker ps -l -q)
{"Status":"exited","Running":false,"Paused":false,"Restarting":false,"OOMKilled":true,"Dead":false,"Pid":0,"ExitCode":0,"Error":"","StartedAt":"2016-06-13T13:33:32.861200851Z","FinishedAt":"2016-06-13T13:33:43.929282195Z"}

Note: "OOMKilled":true

Simulate another memory leak on the heap

This is like before except:

  • we limit the JVM to 32MB of Heap (MAX_HEAP_SIZE_MB=32)
$ docker run -it --memory=64m --memory-swap=64m --env ALLOC_HEAP_MB=1 --env MAX_HEAP_SIZE_MB=32 valentinomiazzo/jvm-memory-test

This is the output:

Native Memory Tracking:

Total: reserved=1315MB, committed=47MB
-                 Java Heap (reserved=32MB, committed=32MB)
                            (mmap: reserved=32MB, committed=32MB)

-                     Class (reserved=1032MB, committed=5MB)
                            (classes #414)
                            (mmap: reserved=1032MB, committed=5MB)

-                    Thread (reserved=6MB, committed=6MB)
                            (thread #11)
                            (stack: reserved=6MB, committed=6MB)

-                      Code (reserved=244MB, committed=3MB)
                            (mmap: reserved=244MB, committed=2MB)

-                    Symbol (reserved=1MB, committed=1MB)
                            (malloc=1MB #103)

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at MemoryTest.main(MemoryTest.java:92)

In this case, as expected, we get an OutOfMemoryException. This is what happened:

  • the GC of the JVM could no allocate another chunk of RAM
  • an OutOfMemoryException was thrown and not caught by the main() method
  • this caused the exit of the JVM
  • the infinite loop calling jcmd was executed yet another time
  • the JVM process was not found by the jcmd loop and therefore the CMD completed
  • the Docker container exited.

You can check that Docker didn't terminate the container with

$ docker inspect -f '{{json .State}}' $(docker ps -l -q)
{"Status":"exited","Running":false,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":0,"ExitCode":0,"Error":"","StartedAt":"2016-06-13T13:39:37.805772027Z","FinishedAt":"2016-06-13T13:40:08.279884682Z"}

Note: "OOMKilled":false

Supported leaks

The image supports the following types of leaks. See the Dockerfile for details about all the available enviroment variables.

  • Heap
  • Direct buffers
  • Native memory via sun.misc.Unsafe.allocateMemory()
  • Classes
  • Threads

Build the image

$ # Let's assume you cloned this repo in jvm-memory-test
$ docker build -t valentinomiazzo/jvm-memory-test jvm-memory-test