Quarkus, jlink and Application Class Data Sharing (AppCDS)

Quarkus, jlink and Application Class Data Sharing (AppCDS)

Quarkus is optimized to start quickly and have a very small memory footprint. This is true when deploying in a standard JVM but even more so when deploying our application as a native executable via GraalVM.

Quarkus greatly facilitates the creation of a native executable, thanks to this, a Quarkus application starts in a few tens of milliseconds and with a very small memory footprint: a few tens of MB of RSS (Resident Set Size – total memory usage of Java the process seen by the OS).

If we take the comparison available on the graph below, we go, for a classic REST JSON / Hibernate stack, from 2s of start-up time to 42ms and from 145 MB of RSS to 28 MB. And only with a single core ! As an example, my applicationbookmark-service of my Quarkus Dojo bookmarkit starts in 1,2s on my laptop.

Another point not mentioned, is the size of the Docker image. A native application will only contain the code used by the application (thanks to the dead code elimination of GraalVM), as well as a minimalist JVM (SubstrateVM), and will therefore be smaller than a standard image which embeds all the third parties libraries as well as a full JVM.

However, deploying your Quarkus application as a native application has a few drawbacks:

  • Because of the close world assumption of GraalVM, all the libraries used must be GraalVM compatible. This is guaranteed for Quarkus extensions, but for others, it’s up to you to check, and there can sometimes be unpleasant surprises.
  • SubstrateVM is a partial JVM, it does not support JMX or JVM-TI (and therefore no Java agents), which can greatly complicate monitoring and administration of your application.
  • A native application has peak performance that is lower than an application deployed in the JVM. Mainly because the Java Just-In-Time Compiler (JIT) will profile your application during its use and will therefore be able to make more relevant optimizations than GraalVM which performs these optimizations during compilation (Ahead-Of-Time compiler – AOT ).

If you want to deploy your Quarkus application in a standard JVM, but optimize its startup time and the size of the Docker image, then Jlink and AppCDS can help you!

In the following paragraphs, I will use the application bookmark-service as example. A JVM at least 11 is necessary (I used OpenJDK 11.0.5).

Jlink

Jlink allows to generate a custom JVM which will contain only the needed modules to make your application runnable and operationnal. This is made possible thanks to the modularization of the JDK which was made for Java 9 (JSR-376 – projet Jigsaw). Who says reduced size of JVM, also says smaller Docker image!

Jlink takes as input the list of modules to include in your customized JVM. To know this list, you must use the tool jdeps.

First of all, you must configure Quarkus to generate a fat jar (or uber jar) so that jdeps can analyze all the code of your application. To do this, add the following property to your application.properties :

quarkus.package.uber-jar=true

We then package the application via mvn clean package.

Then we launch the jdeps command with the --list-deps option which allows you to list the modules on which your application depends.

Here is the result of the jdeps command with the uber jar of the bookmark-service application:

$ jdeps --list-deps target/bookmark-service-1.0-SNAPSHOT-runner.jar 
   JDK removed internal API/org.relaxng.datatype
   java.base/sun.security.util
   java.base/sun.security.x509
   java.compiler
   java.datatransfer
   java.desktop
   java.instrument
   java.logging
   java.management
   java.naming
   java.rmi
   java.security.jgss
   java.security.sasl
   java.sql
   java.transaction.xa
   java.xml
   jdk.jconsole
   jdk.management
   jdk.unsupported

To create your custom JVM, you must use jlink by giving it the list of modules resulting from the call to the command jdeps via the option --add-modules. Small subtlety, the two modules java.base/sun.security.util and java.base/sun.security.x509 must not be integrated, but we must integrate the java.base module instead. I don’t know where this result inconsistency of the jdeps command comes from, but the java.base module is used by any Java application, so it must always be present.

For the bookmark-service application, here is the jlink command that I used:

$ jlink --no-header-files --no-man-pages --output target/customjdk --compress=2 --strip-debug\
--module-path $JAVA_HOME/jmods --add-modules java.base,java.base,java.base,java.compiler,\
java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,\
java.rmi,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,\
java.xml,jdk.jconsole,jdk.management,jdk.unsupported

As I wanted to have the smallest possible JVM, I added the following options to the jlink command:

  • --no-header-files : Do not include header files,
  • --no-man-pages : Do not include man pages,
  • --compress=2 : ZIP level compression for compressible files,
  • --strip-debug : Do not include debug information.

After that, we can use the JVM generated in the target/customjdk directory to launch our application:

$ target/customjdk/bin/java -Xmx32m -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

If we compare the startup of our application with our custom JVM and the standard JVM, there is no advantage to RSS or startup time. The only interest here is the size of the JVM which went from 314MB to 51MB!

Ideally, this should be automated in your Docker build via a multi-stage Dockerfile …

To go further on jlink, I invite you to consult this article: Using jlink to build java runtimes for non modular applications.

AppCDS – Application Class Data Sharing

Warning: if you want to use AppCDS with jlink, you will have to use your custom JVM in the commands of this paragraph instead of your default JVM.

One of the reasons for the consequent start-up time of a Java application is that, for each class, the JVM must load it from disk, verify it, then create a specific data structure (class metadata).

Application Class Data Sharing (AppCDS) is a functionality with which we can create an archive of the metadata of our classes, to avoid doing it at startup. Once these metadatas are loaded, there is no difference in behavior with an application not using AppCDS.

There are three steps for using AppCDS:

Step 1: Creating the list of classes to archive.

java -XX:DumpLoadedClassList=target/classes.lst -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

I stopped the application right after launching it, but we can imagine using it a little longer (launching a functional test for example) to make sure that all the code of the application has been called, and therefore as many classes as possible will be listed.

Step 2: Generating the archive with the JVM option -Xshare:dump which asks AppCDS to create it.

java -Xshare:dump -XX:SharedClassListFile=target/classes.lst -XX:SharedArchiveFile=target/app-cds.jsa --class-path target/bookmark-service-1.0-SNAPSHOT-runner.jar

This command does not launch the application. It will generate a 69MB app-cds.jsa file that we can then use to launch our application.

Step 3: We launch the application by passing it the created archive as an argument.

java -XX:SharedArchiveFile=target/app-cds.jsa -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

With an archive, the application launches in 500ms instead of 1.2s, which makes a -60% startup time!

Unfortunately, there is no difference in terms of memory footprint.

For further reading: Application Class Data Sharing.

Conclusion

Jlink allows you to greatly reduce the size of your Docker image. If you deploy your application as a Docker container and you don’t necessarily control the node on which it is deployed (public cloud or private cloud shared with other applications), this can be interesting to limit the download time of your image. Be careful however to think about re-creating your custom JVM each time you add a new library because it could use a module absent from your previous custom JVM.

AppCDS seems very interesting to me, it greatly reduces the start-up time of your application without any inconvenience other than requiring the creation of the class archive beforehand. On the other hand, the creation of this archive is not trivial, but given the optimization (-60%) it’s worth it!

Special thanks to Logan for proofreading and correcting the many spelling mistakes 😉

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.