In an earlier blog, I wrote about the changes for deployment of Java applications that will take effect in JDK 11. Specifically, the removal of support for Applets and Java Web Start. In this post, I thought I’d delve a bit deeper into the suggested way to ship application code when you make the move to JDK 9 or later, so can take advantage of the Java Platform Module System (JPMS).
JDK 9 introduced the jlink command. This is a tool that can generate a custom Java runtime that only contains the modules that are required for a given application. The fundamental idea of jlink is to enable developers to move away from a centralised Java runtime, with all the associated issues of ensuring the correct version is installed, to one runtime per application (or service if you are using a microservices architecture). For ISVs, in particular, this can eliminate a host of potential support issues generated by not having the right version of the JDK that has been used to test and certify their software.
Initially, you might think that a potential drawback is an increase in size for downloads and the disk space that is used. To some extent this is true, but by eliminating unused modules, the runtime size is likely to be a lot smaller than a full JDK image. The jlink command also comes with a number of options that can help to reduce the size even further. Here are some examples:
--no-header-files
: Excludes header files from the runtime image--no-man-pages
: Excludes man pages from the runtime image--strip-debug
: Strips debug information from the runtime image--compress={0|1|2}
: This allows compression of resources in the runtime image. 0 indicates no compression, 1 indicates constant string sharing and 2 indicates zip compression. It is also possible to add a regular expression to specify a pattern-list of files to include.
There is also an option listed in the documentation, --vm={client, server, minimal, all}
, which gives the impression that you can also reduce the size of the JVM to some extent. Sadly, this is something of a red-herring, as the client and minimal VMs are only available on 32-bit platforms. Oracle no longer provide these binaries from JDK 9 onwards, but Azul will happily provide a 32-bit version of Zulu for both Windows and Linux. I tested the Zulu 9 JDK on Windows 7, and this will allow you to select client, server or all but the minimal configuration does not exist.
To build a runtime image with jlink, you need to specify where to find the modules your application or service requires and the modules you want to install in the runtime. For a fully modularised, application all you need to do is specify the module that contains your main() entry point and jlink will work out the dependency graph for you and include those automatically. You also need to tell jlink where to put the resulting runtime.
I constructed a simple example application that uses three modules called com.azul.zapp, com.azul.zoop and com.azul.zeta. The application makes use of the java.sql package so has a dependency on the java.sql module.
Here’s an example of how to use jlink with this application:
jlink --module-path $JAVA_HOME/jmods:zappmods --add-modules com.azul.zapp --output zappruntime
If we run the java executable in the zappruntime/bin directory with the –list-modules argument we get the following output:
$ zappruntime/bin/java --list-modules com.azul.zapp com.azul.zeta com.azul.zoop [email protected] [email protected] [email protected] [email protected]
Of the 97 modules available in JDK 9, only the four required are included. The result is that the size of the runtime for this (admittedly rather trivial) application is only 47 Mb, including the application code. The full Java SE 9 JDK is 493Mb. If we use the –no-header-files, –no-man-pages and –strip-debug options the runtime size can be further reduced to only 40 Mb. I also tried this on a 32-bit Windows 7 machine and using the –vm=client option available in Zulu was able to reduce this even further to only 33 Mb.
At this point, you might be saying to yourself, “Hang on, Java has always had the marketing slogan of ‘Write once, run anywhere’. If this runtime only includes these modules, surely it’s not an implementation that conforms to the Java specification?” Prior to JDK 9, you would be right. For a runtime to conform to the Java SE specification, as defined by the relevant JSR, all standard libraries had to be present. This was the cause of the long-running legal dispute between Sun Microsystems and Microsoft at the end of the 1990s. Microsoft had decided to “improve” Java by adding some libraries to the java package namespace and leaving out some that they didn’t feel were necessary.
In the Java SE 9 specification things have changed to allow for tools like jlink to produce conforming runtimes. Section 7 of the specification defines the Modular Platform and includes a section on conformance. Here’s the critical part:
“Every Implementor of this Specification must implement the java.base module. Every Implementor may, further, implement a set of one or more additional standard SE modules so long as that set is closed, i.e., contains every standard module required by any member of the set.”
This defines two rules that need to be obeyed by any tool (since you can write your own jlink alternative) that produces a runtime image.
- The java.base module must be included. This is logical because all modules (other than java.base) implicitly have a dependency on java.base. Without it, nothing is going to run.
- If any other modules defined by the Java SE specification are included, then all dependencies of those modules must also be included. This equates to any requires elements of the module-info.java file of these modules. Since those modules may, in turn, have dependencies, the whole dependency graph of the included modules must be present.
If these two rules are followed then the module set in the runtime image generated is classed as closed and thus conforms to the specification.
The jlink tool is great if we have an application that has been developed with explicit modules. What about if we want to use jlink for an older application where everything is still on the classpath?
We can still use jlink, but we need to do a little more work.
We’ll use the same example application but this time remove the module-info.class files from the jars and put them all on the classpath. To use jlink, we need to know what JDK modules we require and we can use jdeps to find this information:
$ jdeps --list-deps -cp zapplib/*.jar Zapp.jar java.base java.logging java.sql unnamed module: zapplib/Zeta.jar unnamed module: zapplib/Zoop.jar
Note that if you don’t set the classpath to include the jar files needed by your application you get this rather confusing output:
$ jdeps --list-deps Zapp.jar java.base java.logging java.sql not found
So, something was not found, but it’s not clear what.
We can now use jlink to build our runtime, thus:
jlink --module-path $JAVA_HOME/jmods --add-modules java.sql,java.logging --output zappruntime
Strictly speaking, we don’t need to specify java.logging as a module to add since it is required by java.sql but, since I used logging in the application I wanted to show the full process.
Checking which modules are in the runtime we get:
$ zappruntime/bin/java --list-modules [email protected] [email protected] [email protected] [email protected]
Lastly, we can copy our jar files into the runtime generated by jlink. For our example we’ll put them all in a directory called dist. Now to run our application we can do this:
$ zappruntime/bin/java -cp ‘zappruntime/dist/*’ com.azul.app.Main
As you can see, whether you want your application to use modules or stick with the classpath for now, it is easy to build a Java runtime tailored to your application. The advantages of doing this are reduced size requirements and eliminating concerns of having the exact version of the JDK to support the application.
With JDK 10 having been released this week, maybe it’s time to make the move to the latest JDK.