Creating Runtime and Application Images with JLink
With the command line tool jlink
you can select a number of modules, platform modules as well as those making up your application, and link them into a runtime image.
Such a runtime image acts like the JDK that you can download but contains just the modules you picked and the dependencies they need to function.
If those include your project, the result is a self-contained deliverable of your application, meaning it does not depend on a JDK being installed on the target system.
During the linking phase, jlink
can further optimize image size and improve VM performance, particularly startup time.
While it doesn't matter much for jlink
it is helpful to distinguish between creating runtime images, a subset of the JDK, and application images, which also contain project-specific modules, so we'll go in that order.
Note:
jlink
"just" links bytecode - it does not compile it to machine code, so this is no ahead-of-time compilation.
Creating Runtime Images
To create an image, jlink
needs two pieces of information, each specified with a command line option:
- which modules to start with /
--add-modules
- in which folder to create the image /
--output
Given these command line options, jlink
resolves modules, starting with the ones listed with --add-modules
.
But it has a few peculiarities:
- services are not bound by default - we'll see further below what to do about that
- optional dependencies are not resolved - they need to be added manually
- automatic modules are not allowed - we'll discuss this when we get to application images
Unless any problems like missing or duplicate modules are encountered, the resolved modules (root modules plus transitive dependencies) end up in the new runtime image.
The Smallest Runtime
Let's have a look at that. The simplest possible runtime image contains only the base module:
# create the image
$ jlink
--add-modules java.base
--output jdk-base
# use the image's java launcher to list all contained modules
$ jdk-base/bin/java --list-modules
> java.base
Creating Application Images
As mentioned before, jlink
doesn't distinguish between modules from the JDK and others, so you can use a similar approach to create an image containing an entire application, meaning it contains application modules (the app itself plus its dependencies) and the platform modules needed to support them.
To create such an image, you need to:
- use
--module-path
to letjlink
know where to find the app modules - use
--add-modules
with the application's main module and others as needed, e.g. services (see below) or optional dependencies
Taken together, the platform and application modules that the image contains are known as system modules.
Note that jlink
only operates on explicit modules, so an application depending on automatic modules can't be linked into an image.
The Optional Module Path
As an example, let's assume the application's modules can be found in a folder mods
and it's main module is called com.example.app
.
Then the following command creates an image in the folder app-image
:
# create the image
$ jlink
--module-path mods
--add-modules com.example.main
--output app-image
# list contained modules
$ app-image/bin/java --list-modules
> com.example.app
# other app modules
> java.base
# other java/jdk modules
Because the image contains the entire application, you don't need to use the module path when launching it:
$ app-image/bin/java --module com.example.app/com.example.app.Main
While you don't have to use the module path, you can, though. In that case, system modules will always shadow modules of the same name on the module path - it will be as if those on the module path don't exist. So you can't use the module path to replace system modules, but you can add additional modules to the application. This will likely be service providers, which allows you to ship an image with your application while still allowing users to easily extend it locally.
Generating a Native Launcher
Application modules can include a custom launcher, which is an executable script (shell on Unix-based operating systems, batch on Windows) in the image's bin
folder that is preconfigured to start the JVM with a concrete module and main class.
To create a launcher, use the --launcher $NAME=$MODULE/$MAIN-CLASS
option:
$NAME
is the file name you pick for the executable$MODULE
is the name of the module to launch with$MAIN-CLASS
is the name of the module's main class
The latter two are what you would normally put behind java --module
.
And like there, if the module defines a main class, you can leave /$MAIN-CLASS
out.
Expanding on the example above, this is how to create a launcher named app
:
# create the image
$ jlink
--module-path mods
--add-modules com.example.main
--launcher app=com.example.app/com.example.app.Main
--output app-image
# launch
$ app-image/bin/app
Using a launcher does have a downside, though:
All options you try to apply to the launching JVM will be interpreted as if you had put them behind the --module
option, making them program arguments instead.
That means, when using a launcher, you can't ad-hoc configure the java
command, for example to add additional services as we discussed earlier.
One way around that is to edit the script and put such options in the JLINK_VM_OPTIONS
environment variable.
Another is to fall back to the java
command itself, which is still available in the image.
Including Services
To enable the creation of small and deliberately assembled runtime images, jlink
, by default, performs no service binding when creating an image.
Instead, service provider modules have to be included manually by listing them in --add-modules
.
To find out which modules provide a specific service, use the option --suggest-providers $SERVICE
, which lists all modules in the runtime or on the module path that provide an implementation of $SERVICE
.
As an alternative to adding individual services, the option --bind-services
can be used to include all modules that provide a service that is used by another resolved module.
Let's pick charsets like ISO-8859-1, UTF-8, or UTF-16 as an example. The base module knows the ones you need on a daily basis, but there's a specific platform module that contains a few others: jdk.charsets. The base module and jdk.charsets are decoupled via services - here are the relevant parts of their module declarations:
module java.base {
uses java.nio.charset.spi.CharsetProvider;
}
module jdk.charsets {
provides java.nio.charset.spi.CharsetProvider
with sun.nio.cs.ext.ExtendedCharsets
}
When the module system resolves modules during a regular launch, service binding will pull in jdk.charsets and so its charsets are always available when launching from a standard JDK.
But when creating a runtime image with jlink
, that does not happen by default, so such images will not contain the charsets module.
If you've determined that you need them, you can simply include the module in the image with --add-modules
:
$ jlink
--add-modules java.base,jdk.charsets
--output jdk-charsets
$ jdk-charsets/bin/java --list-modules
> java.base
> jdk.charsets
Generating Images Across Operating Systems
While the bytecode your application and library JARs contain is independent of any operating system (OS), it needs an OS-specific Java Virtual Machine to execute them - that's why you download JDKs specifically for Linux, macOS, or Windows (for example).
And because that is where jlink
pulls platform modules from, the runtime and application images it creates are always bound to a concrete operating system.
Fortunately, it doesn't have to be the operating system on which you're running jlink
.
If you download and unpack a JDK for a different operating system, you can place its jmods
folder on the module path when running the jlink
version from your system's JDK.
The linker will then determine that the image is to be created for that other OS and will hence create one that works on it (but of course not on another).
So given JDKs for all operating systems your application supports, you can generate runtime or application images for each of them on the same machine.
For that to work without problems, it is recommended to only reference modules from the exact same JDK version as the jlink
binary, so, for example, if jlink
has version 16.0.2, make sure it loads platform modules from JDK 16.0.2.
Let's go back to the application image we created earlier and assume, it is built on a Linux build server. Then this is how to create an application image for Windows:
# download JDK for Windows and unpack into `jdk-win`
# create the image with the jlink binary from the system's JDK
# (in this example, Linux)
$ jlink
--module-path jdk-win/jmods:mods
--add-modules com.example.main
--output app-image
To verify that this image is specific for Windows, check app-image/bin
, which contains a java.exe
.
Optimizing the Image
After learning how to generate an image for or with your application, you can optimize it.
Most optimizations reduce image size and some improve launch times a bit.
Check out the jlink
reference for a full list of options that you can play with.
Whatever options you apply, don't forget to thoroughly test the resulting image and measure actual improvements.
Last review: November 3, 2021
Back to Tutorial List