Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ


Choose your language

InfoQ Homepage Presentations Java in Containers - Part Deux

Java in Containers - Part Deux



David Delabassee looks in parallel at how OpenJDK is evolving to cope with some of those changes and most importantly what it all means for Java developers.


David Delabassee is a Developer Advocate in the Java Platform Group at Oracle. Prior to that, he was involved in Oracle’s Serverless initiatives. He has also been heavily involved in Java EE 8 and its transition to the Eclipse Foundation as part of the Jakarta EE initiative.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.


Delabassee: This session was advertised as Java in Containers, but I changed the title to JVM in Containers, because at the end of the day, what really matters is the Java Virtual Machine that runs in the container. The JVM only understands bytecode. Most of the time that bytecode comes from the javac compiler. If it comes from something else, it doesn't matter.

My name is David Delabassee. I work for Oracle in the Java platform group. I'm remote. You can find me on Twitter @delabassee. The Java platform group is basically the organization within Oracle that is responsible for the Java platform and for OpenJDK. This is a Java track. There is a pattern. You need two things. The first one is a Duke. The second one, anybody can guess what is the second thing that you need in the Java track, a safe harbor statement slide. This is the Oracle one.

Java - 25 Years

Let's discuss JVM and Java. This year is an important year for Java because we're about to celebrate the 25 years anniversary of Java. Since 25 years Java has been developed and has evolved. That evolution is basically based on two core principles. The first one is developer productivity. It has to be easy to write code. Writing code is only one part of the story. Maintainability is also very important. That's why it's also very important to be able to easily write and read the code that has been produced. That's one of the pillars of the evolution of Java.

The second one is application performance. Over the years, we have enhanced the platform to make sure that your code runs in a very efficient manner. We have improved the GC that was present in the early days of the platform. We have added new ones such as G1 is a GC. We have a JIT compiler that allows the Java Virtual Machine to do on the fly optimization. That evolution over the last 25 years has been done based on those two pillars. Keeping in mind that the world around is also evolving. There are new programming paradigms that are appearing or that are getting more popular, so Java has to make sure to cope with the right ones. Lambda Expression introduced in 8, are a good example of that.

Application style, 20 years ago, we were mainly writing monoliths, those days, it's all about microservices and function as a service. Obviously, the platform is able to cope with that as well. The hardware that we run our code on is also evolving a lot. We have more CPUs, more cores. Each CPU has multi-level cache. The platform, the JVM has to be able to cope with that. Last but not least, deployment styles are also evolving. Twenty, 15 years ago, we were deploying monoliths. If we're agile we were deploying once or twice a year into our own data centers. Those days, with CI/CD, we're basically doing multiple deployments a day into containers that are deployed in a Kubernetes cluster in the cloud.

How many of you are using containers today? Basically, a container is a standard way of packaging software with its dependency. There are multiple runtimes: Docker, CRI-O, LXC. At the end of the day, what I'm going to discuss today apply to any OCI compliant containers that is based on namespace and cgroups. Namespace and cgroups are Linux kernels technology that basically gives us the ability to create containers. I'm going to use Docker, but just because I'm used to that. It would also work on Podman or any other runtime.

Container vs. VM

Containers shouldn't be confused with virtual machine. We have containers that are super lightweight. Starting a container takes few milliseconds, depending on the case, but the startup time for containers can be pretty fast. The thing with container is that they're running on some facilities of the underlying Linux kernel, namespace, cgroups, unified systems, to basically give us this idea that our code runs isolated. It's not really isolated because at the end of the day, our containers rely on the underlying kernel host to run the code. That basically means that we are running on the same operating system that we have on the host.

We have virtual machine that provides us real isolation. The thing with VM is that they are very expensive. Starting a VM is a costly operation, so you don't want to do that too many times. Starting a container and killing a container is almost instantaneously. You can do that as many times as you want. Container can become ephemeral, if you want. With VM, we have a strong isolation, but it comes at a cost. With container, we don't have that cost, but we don't have a very good isolation. In between, we have Kata Containers, which basically rely on this lightweight VM to basically provide the benefit of both worlds. We have the isolation of VM with basically the cost and the startup time that we can reach on the container side.

JVM Container Landscape

Java and the JVM. If we look at the JVM container landscape, I think that we're pretty lucky because it's hard these days to find modern frameworks that doesn't support out of the box containers. If you're using Helidon, Micronaut, JHipster, you can directly create a containerized version of your application. On the tooling side, we have a Maven plugin. We have the ability to use Jib, so to create containers using Jib directly from Maven. The IDE, if you take IntelliJ, for example, it supports out of the box the ability to directly create container image.

If we look on the JVM container landscape, it's pretty solid. The first thing that I would like to discuss is JVM container awareness. Since Java 8, we have invested in the JVM to make sure that it runs nicely whenever that JVM run within the containers. There are multiple reasons for that, such as good resource usage. One important reason is what we call the JVM ergonomics. JVM ergonomics is basically something that has been added in Java 8, I believe. It's the ability for the JVM to basically provide a default behavior that works. The JVM, if you don't tune the JVM, will provide you decent behavior. The JVM will look at few metrics, like the number of CPUs, how much memory does it have? Based on that, it will infer some configuration such as what heap size should I use.

To illustrate that, I have a demo. First, I'm going to SSH to a VM in the cloud. What I have here, I have a simple HelloWorld application that basically looks at how many CPUs it has. What is the memory? Some basic metrics. What are the JVM vendors? We can run that, java I'm not compiling the class. I'm directly running it against Java. It's JDK 13.0.2, the vendor is Oracle. It has four CPUs. The three that you see there is the common ForkJoinPool thread, which are configured based on the number of CPUs. Given that we have four CPUs, we have three threads for the common ForkJoinPool. Next I'm going to Dockerize that code. This is my Docker file. It's pretty basic. I take a Java based image, I create a directory, I copy the source, I javac, so I compile that source. I run that. Docker build -t qcon, and this is the context. Let's run that. The image is QCon. This code is running within my container. The Java version is different, it's an older one. It's run inside the container. It has four cores. The common ForkJoinPool is set to 3. If I look at the number of CPUs that my VM has, it has four CPUs, so that match.

What I'm going to do next, I'm going to run the same code, but I'm going to limit the resources that my container can use. This time, instead of four CPUs, my container will only be able to use two. This time, the JVM inside still sees four cores. All the ergonomics will be based on that false assumption. You see that in terms of memory, it has 3 GBs. What I can do, I can limit that memory to 256 MBs. You see that my JVM still sees more than that, and it still sees the 4 CPUs. Something is wrong. Why? Simply because of that. If I look at the Docker file, I'm using this Java latest image, which is completely outdated. I need to fix that. I'm going to simply switch to OpenJDK, latest. Let's build the image again. Let's run the image again with the limitation that we set, 3 CPUs and 256 MBs. This time the JVM sees 3 CPUs, so the common ForkJoinPool will be configured with 2 threads, which makes sense. The runtime and memory is below what the JVM sees. There is a formula that calculates that. This time, we can be sure that our code running in that container will not be killed by the container engine.

In the previous case, the JVM was seeing 4 CPUs, and was also seeing more memory that it has access to, that pretty rapidly the JVM will be killed by the container engine because it will basically try to over-consume the resources that are available. That's why using a JVM that is container aware is important.

Let's talk about performance. Something I didn't show you in the previous example is that the Docker image generated was pretty heavy, in the range of multiple hundreds of megabytes, for a simple Hello World. That's an issue because each time you need to run that container, the container engine needs to fetch that container image from a registry that is hopefully co-located on the same network. Still, to run some very basic application, you'd still need to fetch multiple hundreds of megabytes. That's an issue. We need to work on that.

When it comes to containers, we tend to talk about latency. Latency is basically the time it takes to send the result to the users. We can split that in two. There is the time it takes to start up the containers. Then there is the time it takes to start your application, in this case, the Java Virtual Machine within the containers. If we look at the first layer, the container startup, we need to keep in mind that the container is basically made of layers. We have multiple layers on top of each other that compose our container image. The idea here is to have as small as possible layers. When it comes to JVM container image, we have three types of layers, top to bottom. At the top, we have the Java code or the bytecode with all its dependency. Then we have under that, the Java runtime layer, that's the Java Virtual Machine and something else. Then under that we have the operating system layer. The basic idea is basically to shrink those layers as much as we can.

Java Application Layer

On the Java application layer, there's not much that we can do. There's not much that the JVM can do. We can only give you advice, like watch out for the dependency that you're using. Don't make sure that you're not embedding the whole world with transitive dependency in your code. It's really just best practices. The thing that you need to do is try to leverage the cache mechanism that we have with containers. That's why it's a good idea to keep everything which is relatively static in different layers. That's why maybe Fat JAR are not a good idea when it comes to containers. The thing that the JVM provides is CDS. That's something that you want to leverage.

Java Runtime Layer

At Java Runtime layer, there's something that we can use to shrink the size of that layer, and that is JLink. JLink is a tool that has been added in Java 9. Basically, JLink gives you the ability to create your own custom runtime. Your code doesn't have to be modular for that. You can take an existing Java application and run it on top of a custom runtime created by JLink. To give you an idea of the benefit that we can get using JLink, I took JDK 13, OpenJDK. I started with a full JDK that weighed over 300 MBs. Then you want to create just a runtime, that's also a good idea within the container space. From a security standpoint, it's a good idea to reduce the potential surface attack of your containers. That's why when you're running your code, you don't need for example to have javac, or jmap, all those tools in your containers. They are not needed at runtime. Just get rid of them.

We want to use a Java Runtime instead of a full JDK. With JLink, we have the ability to create that custom runtime, so all the modules. That's basically a Java Runtime that includes all the modules, 168 MBs. This is very stupid to create such a runtime because there is no way any sensible application can make use of all the modules of the platform. That's what I'm doing next. I'm creating a Java Runtime with just the modules that are needed by my code. In this case, this is a serverless Java function. From 168 MBs, I go down to 50 MBs. Then JLink comes with a few additional flags that we can use. We have the ability to remove the header file, the man page. If we do that, we go from 50 MBs to 44 MBs. JLink also provides two levels of compression, compression 2 being Zip Deflate. If we do that, we go from 44 MBs to 34 MBs, basically 34 MBs. I have a custom Java Runtime that includes all the modules and just the modules that are needed by my code. That will greatly reduce the size of those layers.

The thing that we have to keep in mind is that if you look at the bottom, I've used compression just to reduce the size of those layers. Compression means decompression, so maybe it's not a good idea to use compression if we want to save some time, because at runtime, that would imply some decompression cost. Maybe I should have stopped at 44 MBs. That's something that ideally you should measure.

Operating System Layer

At the operating system layer, the JVM cannot do much. You should use optimized distribution, so slim distro, for example. It's not because they're slimming the title of the distribution that it really slims, check that. There's the distroless distro from Google. It weighed almost 200 MBs. It's a bit heavy, but it has Java. The thing is that it's Java 11 only. It has some limitation. Then there are tools such as Docker-slim that in my case doesn't work.

We can go further than that. We can use super optimized Linux distributions such as Alpine. Alpine is an optimized distribution that weighs between 5 MBs and 6 MBs, depending on the version. It's basically a complete Linux distribution to run your code. Obviously, with that size, you don't have all the bells and whistles that you have in any typical Linux distribution. From a container standpoint, we want to reduce as much as we can the potential surface attack. It's good to remove all those bells and whistles that are not needed in any way. The thing is that Alpine rely on musl, musl being the C library that the C, C++ code is using to talk to the Linux kernel. OpenJDK is using libc. OpenJDK is using a different libc to talk to the underlying kernel. You have two options to basically run OpenJDK on top of Alpine. Either you rely on Alpine package glibc, which is basically an intermediation layer that whenever there is a libc call, it will turn that into a musl call. I'm not sure it's the right approach. Or, you can use Project Portola. Project Portola basically compiles OpenJDK on top of musl. You can use OpenJDK on top of Alpine. Today, the current release of Java is Java 13, so we don't really release Portola builds for 14. Why? Because Portola doesn't go through all the testing that we usually do on OpenJDK. You can use it but based on some attention. Having said that, we are always looking for help, to help us maintain that build. If you're willing to help us, you're more than welcome.

JLink Alpine

I'm going to demo that. ITS'DEMO is basically a retail chain in Japan. What I'm going to show you now is basically JLink with Alpine. For that I'm using a server. I'm using the Minecraft server, because it was open-source. What I have here, sever.jar is the Minecraft server from Microsoft, and some of the files that are needed by that server, like the eula.txt if it's not there it won't start the server properties. I have a Docker file to containerize that guy. It's a multistage build. The first build, basically I'm taking 14-Alpine. Then I'm using JLink here to create a custom runtime, compress 2 to compress everything as much as I can. Then I just need to specify which modules are needed by the Minecraft server. It turned out that the Minecraft server needs those 12 modules. There is a specific tool that you can use to figure out which modules are needed by your code. Then the second stage is basically from a pure Alpine distribution. I copy all the files. Those are the Minecraft server files. I'm also copying from the preview stage, the custom runtime. Then I just run that guy.

Docker build -t mine. Using OpenJDK, so the OpenJDK 13 distribution from the OpenJDK community, so the one that is using Oracle Linux. The 12 modules weigh 88 MBs. If I strip-debug, basically if I remove the debug information out of that custom runtime, I would save 14 MBs. If I compress 1, from 88 MBs I would save another 18 MBs. Compress 2, I would save 31 MBs from those 88 MBs. If I remove the header file on the man page, I won't save anything because in that particular distribution, they are not included by default.

Something important, on top you have a custom OpenJDK 13 with one module. OpenJDK 13 from the build that are done by Oracle, it weighs 50 MBs. Then at the bottom, the same custom runtime image, one module, using the Debian OpenJDK 13 build, it weighs nearly 500 MBs. About 10 times the size for the same exact custom runtime. Why? Because the Debian one is embedding the debug symbols. If you're concerned about the size of your Java Runtime layer, you need to clearly remove those symbols. In Java 13, we have added these new capabilities to JLink strip-native-debug-symbols that you can use to basically remove those symbols. If you do that, you will go from 499 MBs to 51 MBs. That's something that you should clearly look at.

I'm building my Minecraft server. Let's look at the size of that image. That Minecraft server, the container weighs 100 MBs. We can look at what's inside, dive mine. Those are the different layers. The first layer is the operating system layers. Then this one is basically just creating a directory. The single layers just had this small text file. This one had the property file. Maybe it could have been a good idea to combine all of those together, but it's not the point here. Then we are adding the server.jar, which is the Minecraft server. That guy weighs 36 MBs. The Minecraft server itself is already 36 MBs. Then the next layer is basically the custom Java Runtime. If we look at the size of that, 58 MBs in that case, that's basically what I had with compression 2 before. Clearly, 100 MBs, we have the operating system Alpine. We have the custom Java Runtime. We have our application that, in this particular case, already weighs 36 MBs. That's something that you should clearly look at.

We've discussed how on the Java side you can improve the startup time of the container. The next step is basically to look at the Java startup time itself. This slide show the startup time between Java 8 and 9. Clearly, it's not on my slide, but smaller is better. We had a big regression between 8 and 9. That's the bad news. The good news is that it has been fixed. This is for a simple HelloWorld application in JDK 14 that will be released in 2 weeks, has been improved, versus 13. I can tell you that in 15, we will be able to shave another extra milliseconds. This is something which is very important in the container space when you are using ephemeral containers. Basically, a container that will be invoked will perform this task and will die shortly. Then the startup time of the application is becoming important. This is the same slide, but with additional data, like Hello World using Lambdas, Hello World using concat strings. Basically, the takeaway is that, if possible, you should look at the recent version of the JVM if the startup time is something which is important to you.

Something else that we can use is CDS. The previous example was quite visible because we just had the startup time of the application and the application was very trivial. CDS is something that we can use for non-trivial applications. Anybody knows what CDS is, Class Data Sharing? Which is very low, because CDS is not a new capability, CDS is in the platform since Java 5. The basic idea with CDS is the following. When you run a Java application, the JVM will load the bytecode from disk. I have to perform a bunch of operation before it has something that the JVM can use, from the memory. With CDS, the JVM will perform that. Once it has the in-memory representation of your bytecode, it will dump that to disk, so that the next time you use your application, the JVM will directly bring that into memory. It doesn't have to go through all the operation, which are expensive. The thing is that obviously you have a lot of classes. That's the basic idea with CDS. In the early days, CDS was quite limited. CDS for Java SE 5 was just for the runtime classes, so the rt.jar, back then. It was limited, I think, to Serial GC. Basically, all those limitations have been lifted since then.

CDS Demo

Let's have a quick CDS demo. For that, I'm going to go back to my Linux box. I'm going to compile that HelloWorld application, Java HelloWorld. I'm going to time that invocation, time HelloWorld, so 115-something milliseconds. To get some more accurate number I'm going to invoke that application many times without and with CDS. For that I have a nice tool, which will invoke the application 42 times, perf42. For the first round, I'm going to disable CDS. To disable CDS, I need to use that flag. The application is invoked 42 times, and on average, it takes 220 milliseconds. Now I'm going to use CDS. I'm going to do 42 invocations but with CDS. To use CDS, it's pretty easy. Remove that guy, because now CDS is enabled by default, so 222 versus 157. This time, we're at 71% of the startup time than the startup time without CDS. That's something that you get for free. It's in the platform. You might say, "This is good. It's a huge win, but it was a trivial application".

This tweet is coming from the JRuby project lead. He works at Red Hat. Basically, the number that they have now with JDK 14 using App CDS, jdk-08.202, 853 milliseconds, now with jdk-13, 717 milliseconds. The gap is quite important. JRuby is not a trivial application. It's really a complex application. In containers, when startup time is important, that's something that you should consider, using CDS. Application CDS is basically the ability that CDS gives you to dump all your classes of your own code. Your own application, you can create an archive for CDS.

CDS introduced in Java 5. It has been open-source, I think, in 10, with application CDS. In Java 9, it was a commercial feature. It has been open-sourced in 10. We're keeping to improve CDS. For example, in 13, we have added Dynamic CDS, which gives the ability to create the archive of your classes when your application exits. Basically, you run your application once and at the end when it exits, it will create an archive with all the classes.


You can go further than that, if the startup time and the footprint is important. You can look at GraalVM. GraalVM is a project run by Oracle Labs. It's a high performance polyglot virtual machine that provides many capabilities. On the Java side, there's this polyglot API that you can use to interact with different language. They have a JIT compiler, which is written in Java. It's a JIT compiler that you can use to plug and replace C2 within HotSpot. The nice thing about that is that the JIT compiler is written in Java. It's very performant. Then they have these native-image capabilities, which basically allow you to take some Java application, so some bytecode, and turn that into a native executable for a given platform.

Let's have a look at GraalVM. Java HelloWorld. First, I compile the application and then I'm using this native-image tool from Graal to AOT compile it. Basically, what I give to native-image is a simple class for my application. I'll give it an application. It will go through this native-image tool that will scan the code to find all the potential execution paths. Then it will remove all the dead branches. Once it has that, it will turn that code into native code. Machine code that will run on a given platform, in this case, it's a Mach platform. I will have in term of output, Mach executable. It's done. It took 42 seconds. If we look here, we have this HelloWorld file. If we look at the type of that file, it's Mach OS 64-bit executable that I can obviously invoke. You see the startup time is pretty fast, 13 milliseconds. That's pretty fast. If we look at the size of that executable, 8 MBs. That's the only thing I need. There is no external Java Runtime that is needed.

If I compared that to pure Java, for that I'm going to switch to a different version of Java. Let's first compile the class with that version, java HelloWorld. Let's time that, 143 minutes. You see that the Java version is different. This is Java 14. To be fair with you, I have a bunch of JDK installed on my machine, so that has a bit of time. If I want to get a more accurate result, I need to invoke directly the right JDK, JDK-14 RC1, home/bin/java HelloWorld. I need to time that. I was at 145 milliseconds and this time I'm at 93, 93, 83. That's a more accurate number. Still, we don't have the performance in terms of time that we have with Graal, basically. Why? Because here we have a full-blown JVM, we have HotSpot with all the capabilities of HotSpot.

That's GraalVM native-image capabilities. There are a few limitations. It only supports Java 8 and 11. There are also some limitation, mostly supported basically mean that it works but you need to configure that, the use of a JNI, for example. Then there are a few things that are not supported. The thing is that, what you'll get in terms of output is native executable. You lose the portability of Java, you don't have a JAR. You have a native executable. That's something that you have to keep in mind. You will also lose the ability to use some of the tooling that you might have on the Java side, for managing or observing your application, JMX, those things might not be available. That's something that you have to keep in mind. Still, if the startup time is something important, you need to look at GraalVM native-image. The thing also with GraalVM native-image is that the memory footprint will be lower. Something that Graal doesn't have is for example the GC and G1. They have a low latency G1 based GC that has been introduced. I'm not sure it's the full features yet. Clearly, they don't have all the capabilities that we have with HotSpots in terms of GC, for example.


Talking about GC, in 14 we have improved G1 GC with NUMA capabilities. NUMA stands for non-uniform memory access. It basically means that your memory is not always equidistant to the CPU. It's a good idea for when you access something in memory that your code is aware of that. The GC is now able to deal with NUMA architecture.

Something else that is not clearly advertised is the older improvement that we're doing into the platform. Between Java 8 and 14, there are over 700 enhancements that have been done just for G1. All together, they are bringing extra new capabilities. One that I show on this slide is the memory footprint of G1 for native heap, in this case of 16 MBs. The extra overhead in JDK 8 for 16 GBs, it was 4 GBs of extra memory. In 11, we were just below 3. In 14, we are below 2. Basically, between 8 and 14, we have dropped by 2, the extra footprint needed to deal with a 16 GB heap. Not only that, we have also increased the performance of G1. Those are the features that are not advertised, but that you need to keep in mind when looking at a new version of Java. That's something which is also important when your JVM run into containers. The footprint is obviously very key.


Let's talk about security. You might have seen this tweet that says that the top 10 most popular Docker containers each contain at least 30 vulnerabilities. The good news, Java is not there. That's the good news. The bad news is that we're not immune. This is something that popped up on the OpenJDK mailing list. In that case, that was Debian, they released a 11-something GA release, even before it was officially GA. The source code is there so anyone can build from the source code and claim it's GA. Watch out.

Two weeks ago I saw that. Someone was looking for openjdk-11-GA Linux build for musl. The thing is that there's no such GA build. Nevertheless, someone figured that if you take that particular build, you can consider it as the OpenJDK-11 Alpine General Availability release, even though that release doesn't exist. Then you look at the Docker file. It's very small, but basically, the Docker file is doing a wget from a file coming from a university in Austria over HTTP. The good news is that they are checking the SHA-256 checksum, but from the same source over HTTP. It's on the internet, so it should be true. Then other projects were using that as the 11-GA Linux. My advice here, when it comes to Java, but also container in general, you should choose your base image wisely. Not only that, you should secure it. You shouldn't trust someone else to do it. Don't believe what you read on the internet.

Rootless Container

There are these new trends, where it's a good idea to run rootless containers. Basically, we want to give as less privilege to the container as we can. It's a bit tricky to do. Docker, they have a rootless mode. The last time I checked it was still in experimental features. If you want to do pure rootless containers, you need to use Podman. For that Podman, use cgroups v2, which is not something new. Cgroups v2 has been added in the Linux kernel six years ago. It was not enabled by default. It's now enabled by default on Fedora. Fedora is the first major distribution that enables cgroups v2 support by default. With that, you can easily do rootless container. On the JDK side, you need JDK 15. JDK 15, we had support for cgroups v2. I put a little asterisk, remember the first slide the disclaimer slide that basically says that it's there, the day is there, but that's really JID. JDK 15 would support cgroups v2. If cgroups v2 are not supported on the platform, it will automatically fall back to cgroups v1 as today.

Common Sense

Then in terms of security, it's just common sense. Choose your base image wisely. The rest is just pure container. Watch out that the certificates that you have in your container image there are a bunch of security offerings of tools that you can use to basically scan your image for vulnerabilities. Also, it's container. The basic idea, you should try to reduce as much as you can the potential surface attacks. On the Java side, we have JLink that is useful to achieve that.

The JDK comes with a bunch of tools, jcmd, jinfo, jps, jmap. The thing that we tend to forget is that we can also use them either on the host. To do that, obviously, the host needs to have privilege access. It's not necessarily a good idea. You can also run them within the containers, doing a Docker exec, and then the name of the container, and then for example jinfo, doing that you can get useful information about the JVM that is running inside your container without having to do anything else. That's something that you should try to leverage.

Something else that is coming in the JDK since 11 is JDK Flight Recorder. JFR is basically a black box that is built into the JVM. The JVM is emitting event. It's very low overhead. That's something that you can use in production. Then you have the ability to analyze those events. Until now, those events, the analysis of those events were basically done postmortem, or after the fact. You start the recording, you stop the recording, you don't do recording, and then you do the analysis. In JDK 14, we have the ability to stream the event as they happen. You can analyze the event in process, doesn't really make sense in this case, but you can also analyze the event outside of the process. What you can have is you have your container that is using JFR. It's low overhead. That's something that you can have on all the time. For example, the repository is basically where the events are being put, can be in the volumes that you can access from different containers or from the host. That means that you can now start to think about scenarios that are using JFR to basically emit event from the JVM that is running within the containers and consume them outside. That's something which is new in JDK 14.


JVM in containers, the JVM needs to behave as a good container citizen. The advice here is, you need to use a recent version of the JVM. Don't rely on an old version of the JVM like I did at the beginning. Then I'll discuss quickly some tricks and tools that we can use to reduce latency. That's something which is important in the container space, keeping in mind that there are two types of latency. First, container latency, so the time it takes to start the container, then the Java application latency. Then something which is important is that all the other investment that we're doing into OpenJDK are also leaking into containers. If I take for example, Loom, Panama, ZGC, all those features which are not by design, conceived for containers, will also work nicely when they are being deployed into containers. That's also something that you need to look at over time.

JVM in containers, choose your base image wisely, secure it. Use the latest Java version, and never use Java latest, that base image. We're trying to get rid of that one, but it's very difficult. That's only something that you can use on stage when you're doing demo and you want to clearly make a point to not use it. Only rely on actively supported version. They are all container aware. JVM ergonomics will kick in. There is this flag that I often see, use container support. Those there, you don't need to use that flag anymore because the JVM is by default using the container support. You should only use that flag if you explicitly want to disable the container support, assuming you have a valid reason. Then, don't use a full JDK, but try to use a JRE, or a custom Java Runtime for your code that is running into containers.

Questions and Answers

Participant 1: You mentioned that one of GraalVM's limitations is invokedynamic. All of invokedynamic?

Delabassee: No. It's just a copy and paste from their site, invokedynamic works, but something that I didn't really check. That's a copy and paste from the GitHub page from Graal, but no. I want to make sure that I use the right limitation that they're saying.

Participant 1: Lambdas work?

Delabassee: Yes.

Participant 2: I just wanted to ask you about the thing that you mentioned with all the official supported JVMs being container aware. Is that valid from Java 8 onwards for all of them?

Delabassee: Is Java 8 supported into containers?

Participant 2: As in, container aware.

Delabassee: I believe it's starting from 8, 199. My advice if you're using Java 8, use the latest version of Java 8. The container support has been added in, I think 199. Yes. If you use a recent version of 8, you're on the good side. Having said that, it's a good idea also to look at 11 and more for just the extra features that are provided.

Participant 2: I was just wondering if you still need to use the two flags, ExperimentalVMOptions in the cgroup, or something.

Delabassee: No. Those have been removed. You need to look if it's in 199 or in 210. Those flags are removed. The container support is by default.


See more presentations with transcripts


Recorded at:

May 19, 2020