Jlink in Java: Custom Runtime Images

Naveen AnamaneniUpdated · 5 min read

jlink is a JDK tool that builds a custom runtime image: a folder containing a cut-down Java runtime with only the modules your application needs, plus your application's own modules, and a launcher to start it. The result runs on a machine that has no Java installed. A full JDK is hundreds of megabytes; an image for a small program can be a fraction of that. This is one of the practical payoffs of the module system: because every module declares what it requires, jlink can work out exactly which parts of the JDK to include. In this lesson, you will learn how jlink works, its main options, and complete this chapter's checkpoint by shipping the modular expense tracker as its own runtime.

What Goes into an Image

InputOutput
Your modular JARs (--module-path)A folder with bin/, lib/, conf/ and a release file
The root module to start from (--add-modules)Only the modules reachable from that root
Optional launcher nameA script in bin/ that starts your application

Options

OptionDescription
--module-path <path>It is used to say where your modules are.
--add-modules <names>It names the root modules; everything they require is added automatically.
--output <dir>It sets the folder to create. It must not exist yet.
--launcher <name>=<module>It creates a start script bin/<name> for a module's main class.
--strip-debugIt removes debugging information to save space.
--no-header-files, --no-man-pagesThey leave out files only needed for development.
--compress <level>It compresses the image's contents.
--bind-servicesIt also includes modules that provide services the image uses.

Checkpoint: Shipping the Expense Tracker

The starting point is the two modular JARs built in "JAR Files and Executable JARs in Java": mods/expenses.core.jar and mods/expenses.app.jar.

Step 1: check the dependencies.

Terminal
jdeps --module-path mods -s mods/expenses.app.jar
Terminal output
expenses.app -> expenses.core
expenses.app -> java.base

The application needs only its own two modules and java.base.

Step 2: build the image.

Terminal
jlink --module-path mods --add-modules expenses.app \
    --launcher expenses=expenses.app \
    --strip-debug --no-header-files --no-man-pages \
    --output image

Step 3: look at what was created, and run it.

Terminal
ls image image/bin
image/bin/expenses
image/bin/java --list-modules
Terminal output
image:
bin
conf
legal
lib
release
 
image/bin:
expenses
java
keytool
 
By category: {Food=649.75, Travel=1200}
Total: 1849.75
Running in module: expenses.app
 
expenses.app@1.0
expenses.core@1.0
java.base@25.0.5.0.3

Explanation:

  1. --add-modules expenses.app names the root. jlink reads its module-info, follows requires to expenses.core, and from there to java.base. Nothing else is included.
  2. --launcher expenses=expenses.app creates the script image/bin/expenses. It works because the JAR records the module's main class.
  3. image/bin/expenses starts the application using the image's own java. No installed JDK is involved.
  4. --list-modules confirms the image holds exactly three modules. The full JDK has 69.

Step 4: compare sizes.

Size on this machine
The full JDK 25371 MB
The custom image42 MB

Your numbers will differ by platform and JDK build, but the image is typically a small fraction of the JDK.

What You Should Know Before Using It

PointDetails
The image is platform-specificAn image built on macOS runs on macOS. Build on, or for, each target system (cross-building needs that platform's JDK jmods).
Everything must be a real modulejlink doesn't accept automatic modules. A dependency without module-info blocks it; build tools have plugins that work around this.
You own the updatesThe image contains a fixed JDK version. Security fixes arrive only when you rebuild and redistribute.
It isn't an installerIt is a folder. To make a .dmg, .msi or .deb, use jpackage (see "Jpackage in Java: Native Installers").

When to Use It

Good fitPoor fit
Command-line tools and desktop applications sent to usersApplications deployed to a server that already has a managed JDK
Container images, where every megabyte countsProjects whose dependencies aren't modular
Devices with little storageCode that changes daily, where rebuilding images is a burden

For containers, a common pattern is a build stage that runs jlink, and a final stage that copies only the image into a minimal base image.

Common Mistakes

An output folder that already exists. jlink refuses; delete it first.

Forgetting a service provider. Modules found only through ServiceLoader (such as some charset or security providers) aren't pulled in by requires. Add them with --add-modules or use --bind-services.

Expecting one image for all operating systems. Each platform needs its own.

Automatic modules on the module path. jlink stops with an error naming the offending JAR.

Practice

  1. Easy: Build the image without --strip-debug --no-header-files --no-man-pages and compare its size.
  2. Harder: Add a third module to the expense tracker that uses java.net.http, rebuild the image, and see which extra JDK modules appear in --list-modules.

Quick check: jlink

4 questions
1What does jlink produce?
2How does jlink know which JDK modules to include?
3Does the target machine need Java installed to run the image?
4Can an image built on macOS run on Linux?

Key Takeaways

Was this tutorial helpful?

Lesson 518 of 578