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 😉