Java in a world of containers: Meetup notes

Recently, I went to a meetup where Mikael Vidstedt, Director at Oracle, gave a talk titled “Java in the world of containers.” This post is a summary of the notes I took during the talk.

Java is a great fit for a containerized world

  • Java’s hardware and OS agnostic nature makes it ideal for running in heterogenous environments.
  • Security is enforced by the JVM
  • Great instrumentation/monitoring tools for observability
  • Managed runtime means that it can adapt to changes in environment
  • Importantly, Oracle is committed to making Java the primary choice for container deployments

Custom JREs

A major concern in containerizing a Java app is size. The full JDK is ~600MB and your application probably doesn’t depend on the full JDK. The good news is the module system introduced in Java 9 makes it possible to build a custom JRE with only the modules your app needs.

An example of using jlink to create a minimal JRE with only the (mandatory) java.base module:

jlink --output my-jre --add-modules java.base

This alone may be enough for a few applications, but a more representative set is the modules required by the popular open source library Netty. This comes in at about 60MB. The size can be further reduced by using jlink --compress which enables resource compression. However, this may result in a performance hit depending on your application.

To list out your application’s dependencies, you can use the handy jdeps tool. A nice tip that Mikael gave – in JDK 10, a --print-module-deps flag has been added to jdeps which outputs the dependencies in a comma-separated list. Thus, you can automate the creation of a custom JRE for your app with something like:

 jlink --output my-jre --add-modules $(jdeps --print-module-deps myapp.jar) 

If you’re a stickler for pain, you can use jlink --strip-debug to further reduce the JRE size at the expense of reduced debuggability (you most likely don’t want to do this!). Mikael’s team also experimented with removing JIT compilers and keeping a single GC implementation in the JRE. Again, you may not want to do unless you really want to trade performance for smaller size.

Optimizing base OS image size

The container world has moved to using extremely small, stripped down Linux distributions like Alpine which is the official distribution for Docker images. The base Alpine image comes in at a miserly 4MB.

Unfortunately, OpenJDK is not compatible with musl-libc, a lightweight alternative to glibc that Alpine is based on. Project Portola provides a port of the JDK (9+) that is compatible with musl-libc and hence Alpine. Incidentally, Mikael is the project lead on Portola.

Cross-container sharing

The total size can be by amortized over the set of containers running on the same host OS by sharing data. Class Data Sharing which has been around for a while, allows you to create a memory-mapped archive of JDK classes that can be shared among JVM (and container) instances.

What about application classes? Enter AppCDS. AppCDS reduces both startup time and application footprint. For example, using AppCDS for WebLogic, Mikael’s team was able to reduce startup time by ~48% and memory footprint by ~10%.

Honoring cgroup limits

The JVM chooses default values for heap size, GC tuning etc. based on the underlying system configuration. Linux containers are governed by cgroups which makes it possible to do strict resource allocation. For example, a container may be granted permission to use a single CPU on a 4-core system. Until recently, the JVM completely ignored these resource limits, causing confusion. (BTW, the same issue also affects linux commands like free and top.

JDK10 includes changes that honor these resource limits. Full details can be found on Matthew Gilliard’s excellent blog post. Most importantly, Runtime.availableProcessors() will return the correct value inside a container which means threadpool sizes (eg. ForkJoinPool) computed based on it will be configured correctly.

Summary

The JDK team is constantly making improvements to make Java container-friendly. There is a draft JEP tracking the changes needed, many of which have been already implemented.

Thanks to Mikael Vidstedt for reviewing this summary
PS: Mikael has posted the full slide deck here

Want more content about writing modern and lean Java?