The Introduction

Picture an infrastructure with loosely coupled micro services, in a docker swarm environment. The services have REST end points and we use Spring Boot to realise these services.

Looking at the memory usage of these services however, being around 400MB each, they hardly qualify as ‘micro’ services.

This led to an investigation into the memory usage of Spring Boot. And a search for possible alternatives for Spring Boot.

The memory usage of spring is described by Dave Syer here, and here for Spring Boot in docker. A comparison of the various REST frameworks can be found here, but most comparisons don’t specifically look at memory usage.

So I decided to take 6 popular java frameworks with a REST feature, set up a simple “Hello World” endpoint on them and measure the memory usage with a small load on the endpoint.

That way the memory usage of the framework itself can easily be measured.

I picked Spring Boot (obviously), DropWizard, Play, Vertx, Spark (Java) and Jersey (JAX-RS reference implementation) running on Tomcat 8.

The Setup

All the apps have a minimum setup, following the guidelines of the framework. They all have a simple get endpoint returning a string. For example for SpringBoot:

@RestController public class HelloController { @GetMapping(value = "hello") public String hello() { return "hello-world"; } } 1 2 3 4 5 6 7 8 @RestController public class HelloController { @GetMapping ( value = "hello" ) public String hello ( ) { return "hello-world" ; } }

and for Spark :

public static void main(String[] args) throws UnknownHostException { get("/hello", (req, res) -> "Hello World"); } 1 2 3 public static void main ( String [ ] args ) throws UnknownHostException { get ( "/hello" , ( req , res ) -> "Hello World" ) ; }

which is probably the shortest java definition of the service of all frameworks.

Then a runnable jar is made which is done with maven-shade plugin for Spark, Vertex and Dropwizard, with the spring-boot maven plugin for Spring Boot.

Then the docker file to make the docker image:

FROM openjdk:8-jdk COPY ./target/minimal-boot-1.jar /usr/app/boot.jar WORKDIR /usr/app HEALTHCHECK --interval=5s CMD curl -f http://localhost:8080/hello || exit 1 CMD java -XX:NativeMemoryTracking=summary -jar boot.jar 1 2 3 4 5 FROM openjdk : 8 - jdk COPY . / target / minimal - boot - 1.jar / usr / app / boot . jar WORKDIR / usr / app HEALTHCHECK -- interval = 5s CMD curl - f http : / / localhost : 8080 / hello || exit 1 CMD java - XX : NativeMemoryTracking = summary - jar boot . jar

Here we use the docker health check with curl to check the service running correctly and to give a steady load, calling the service every 5 seconds.

The NativeMemoryTracking option is added to inspect the memory usage of this container in more detail (see results).

For Play an existing docker image on docker hub is used and extended, because of the hassle with nbt to make a runnable jar.

For Tomcat an existing docker image is used and extended adding curl for the health check and the generated war of the application:

FROM tomcat:9.0-jre8-alpine RUN apk add --update curl && rm -rf /var/cache/apk/* COPY target/minimal-jersey-1.war /usr/local/tomcat/webapps/jersey.war HEALTHCHECK --interval=5s CMD curl -f http://localhost:8080/jersey/hello/ || exit 1 CMD ["catalina.sh", "run"] 1 2 3 4 5 FROM tomcat : 9.0 - jre8 - alpine RUN apk add -- update curl && rm - rf / var / cache / apk / * COPY target / minimal - jersey - 1.war / usr / local / tomcat / webapps / jersey . war HEALTHCHECK -- interval = 5s CMD curl - f http : / / localhost : 8080 / jersey / hello / || exit 1 CMD [ "catalina.sh" , "run" ]

Then a compose file is used to start it all up :

version: '3.1' services: springboot: image: boot environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" play: image: play environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" dropwizard: image: drop environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" vertx: image: vertx environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" spark: image: spark environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" jersey: image: jersey environment: - "_JAVA_OPTIONS=${JAVA_OPTIONS}" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 version : '3.1' services : springboot : image : boot environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}" play : image : play environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}" dropwizard : image : drop environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}" vertx : image : vertx environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}" spark : image : spark environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}" jersey : image : jersey environment : - "_JAVA_OPTIONS=${JAVA_OPTIONS}"

Here the JAVA_OPTIONS parameter is used to give all apps the same heap space properties,e.g.:

export JAVA_OPTIONS="-Xmx500m -Xms50m" docker-compose up -d 1 2 export JAVA_OPTIONS = "-Xmx500m -Xms50m" docker - compose up - d

The Results

For memory usage insights look at docker stats. When running the applications without heap constraints on a 2GB docker environment you’ll find something like this after a few minutes:

NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS rest_springboot_1 2.83% 248.3MiB / 1.952GiB 12.42% 1.53kB / 0B 21.3MB / 0B 33 rest_spark_1 2.67% 31.96MiB / 1.952GiB 1.60% 1.71kB / 0B 12.5MB / 0B 27 rest_vertx_1 2.70% 47.8MiB / 1.952GiB 2.39% 1.78kB / 0B 22.4MB / 0B 21 rest_jersey_1 0.14% 129.7MiB / 1.952GiB 6.49% 1.71kB / 0B 25.3MB / 0B 49 rest_play_1 4.16% 176.5MiB / 1.952GiB 8.83% 1.46kB / 0B 29.6MB / 32.8kB 33 rest_dropwizard_1 3.19% 139.2MiB / 1.952GiB 6.97% 1.78kB / 0B 16.7MB / 0B 35 1 2 3 4 5 6 7 8 NAME CPU % MEM USAGE / LIMIT MEM % NET I / O BLOCK I / O PIDS rest_springboot _ 1 2.83 % 248.3MiB / 1.952GiB 12.42 % 1.53kB / 0B 21.3MB / 0B 33 rest_spark _ 1 2.67 % 31.96MiB / 1.952GiB 1.60 % 1.71kB / 0B 12.5MB / 0B 27 rest_vertx _ 1 2.70 % 47.8MiB / 1.952GiB 2.39 % 1.78kB / 0B 22.4MB / 0B 21 rest_jersey _ 1 0.14 % 129.7MiB / 1.952GiB 6.49 % 1.71kB / 0B 25.3MB / 0B 49 rest_play _ 1 4.16 % 176.5MiB / 1.952GiB 8.83 % 1.46kB / 0B 29.6MB / 32.8kB 33 rest_dropwizard _ 1 3.19 % 139.2MiB / 1.952GiB 6.97 % 1.78kB / 0B 16.7MB / 0B 35

You discover immediatly that Spring has the most memory usage, while Spark uses the least.

To analyse the effect of the heap size we do the same measurement with fixed heap size (-Xms and Xmx = 50M), and get the following results :

NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS rest_springboot_1 0.16% 151MiB / 1.952GiB 7.56% 1.65kB / 0B 0B / 0B 33 rest_spark_1 0.15% 35.68MiB / 1.952GiB 1.78% 1.65kB / 0B 0B / 0B 27 rest_vertx_1 0.10% 50.94MiB / 1.952GiB 2.55% 1.73kB / 0B 0B / 0B 20 rest_jersey_1 0.17% 110.8MiB / 1.952GiB 5.55% 1.73kB / 0B 0B / 0B 49 rest_play_1 1.10% 88.76MiB / 1.952GiB 4.44% 1.65kB / 0B 0B / 32.8kB 33 rest_dropwizard_1 0.17% 127.3MiB / 1.952GiB 6.37% 1.82kB / 0B 12.3kB / 0B 34 1 2 3 4 5 6 7 8 NAME CPU % MEM USAGE / LIMIT MEM % NET I / O BLOCK I / O PIDS rest_springboot _ 1 0.16 % 151MiB / 1.952GiB 7.56 % 1.65kB / 0B 0B / 0B 33 rest_spark _ 1 0.15 % 35.68MiB / 1.952GiB 1.78 % 1.65kB / 0B 0B / 0B 27 rest_vertx _ 1 0.10 % 50.94MiB / 1.952GiB 2.55 % 1.73kB / 0B 0B / 0B 20 rest_jersey _ 1 0.17 % 110.8MiB / 1.952GiB 5.55 % 1.73kB / 0B 0B / 0B 49 rest_play _ 1 1.10 % 88.76MiB / 1.952GiB 4.44 % 1.65kB / 0B 0B / 32.8kB 33 rest_dropwizard _ 1 0.17 % 127.3MiB / 1.952GiB 6.37 % 1.82kB / 0B 12.3kB / 0B 34

Now we see Spring Boot still using a large amount of memory, far more than the heap space usage. While Spark and Vertx use very little of the heap and have a very small memory footprint.

To analyse this large memory usage of Spring we use the native memory tracking java tool:

docker exec rest_springboot_1 jcmd 6 VM.native_memory summary 1 docker exec rest_springboot_1 jcmd 6 VM . native_memory summary

giving:

Total: reserved=1443704KB, committed=158176KB - Java Heap (reserved=51200KB, committed=51200KB) (mmap: reserved=51200KB, committed=51200KB) - Class (reserved=1083313KB, committed=36657KB) (classes #5470) (malloc=6065KB #6169) (mmap: reserved=1077248KB, committed=30592KB) - Thread (reserved=33037KB, committed=33037KB) (thread #33) (stack: reserved=32896KB, committed=32896KB) (malloc=103KB #166) (arena=38KB #64) - Code (reserved=251589KB, committed=12717KB) (malloc=1989KB #3475) (mmap: reserved=249600KB, committed=10728KB) - GC (reserved=7648KB, committed=7648KB) (malloc=5772KB #159) (mmap: reserved=1876KB, committed=1876KB) - Compiler (reserved=145KB, committed=145KB) (malloc=14KB #152) (arena=131KB #3) - Internal (reserved=6461KB, committed=6461KB) (malloc=6429KB #7119) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=8969KB, committed=8969KB) (malloc=6212KB #56128) (arena=2757KB #1) - Native Memory Tracking (reserved=1156KB, committed=1156KB) (malloc=6KB #74) (tracking overhead=1150KB) - Arena Chunk (reserved=186KB, committed=186KB) (malloc=186KB) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Total : reserved = 1443704KB , committed = 158176KB - Java Heap ( reserved = 51200KB , committed = 51200KB ) ( mmap : reserved = 51200KB , committed = 51200KB ) - Class ( reserved = 1083313KB , committed = 36657KB ) ( classes #5470) ( malloc = 6065KB #6169) ( mmap : reserved = 1077248KB , committed = 30592KB ) - Thread ( reserved = 33037KB , committed = 33037KB ) ( thread #33) ( stack : reserved = 32896KB , committed = 32896KB ) ( malloc = 103KB #166) ( arena = 38KB #64) - Code ( reserved = 251589KB , committed = 12717KB ) ( malloc = 1989KB #3475) ( mmap : reserved = 249600KB , committed = 10728KB ) - GC ( reserved = 7648KB , committed = 7648KB ) ( malloc = 5772KB #159) ( mmap : reserved = 1876KB , committed = 1876KB ) - Compiler ( reserved = 145KB , committed = 145KB ) ( malloc = 14KB #152) ( arena = 131KB #3) - Internal ( reserved = 6461KB , committed = 6461KB ) ( malloc = 6429KB #7119) ( mmap : reserved = 32KB , committed = 32KB ) - Symbol ( reserved = 8969KB , committed = 8969KB ) ( malloc = 6212KB #56128) ( arena = 2757KB #1) - Native Memory Tracking ( reserved = 1156KB , committed = 1156KB ) ( malloc = 6KB #74) ( tracking overhead = 1150KB ) - Arena Chunk ( reserved = 186KB , committed = 186KB ) ( malloc = 186KB )

Looking at the committed memory we see the spring has loaded a lot of classes (5470, using 36MB) and lots of space for threads (33MB).

Also the code section (generated code by cglib etc.) is contributing with 12MB. This the effect of the large library of the spring components used by springboot.

We also analysed the effect of a greater payload. We varied the payload from 1 per second to the maximum payload the CPU usage of the component would allow in our setup. This was mostly around 1000 request per second. For each measurement we waited for the memory usage to be constant, which sometimes decreased due to garbage collection. In the result we see that Vertx and Spark stayed constant a a low memory usage. Also is seen that Play’s memory usage is increasing significant due to the greater payload.

In Conclusion

If a real micro service with a small memory footprint is what you are looking for Spark and Vertx are the best candidates of the 6. Spring Boot proved to be the worst.