Jlink in Java: Custom Runtime Images
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
| Input | Output |
|---|---|
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 name | A script in bin/ that starts your application |
Options
| Option | Description |
|---|---|
--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-debug | It removes debugging information to save space. |
--no-header-files, --no-man-pages | They leave out files only needed for development. |
--compress <level> | It compresses the image's contents. |
--bind-services | It 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.
jdeps --module-path mods -s mods/expenses.app.jarexpenses.app -> expenses.core
expenses.app -> java.baseThe application needs only its own two modules and java.base.
Step 2: build the image.
jlink --module-path mods --add-modules expenses.app \
--launcher expenses=expenses.app \
--strip-debug --no-header-files --no-man-pages \
--output imageStep 3: look at what was created, and run it.
ls image image/bin
image/bin/expenses
image/bin/java --list-modulesimage:
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.3Explanation:
--add-modules expenses.appnames the root.jlinkreads itsmodule-info, followsrequirestoexpenses.core, and from there tojava.base. Nothing else is included.--launcher expenses=expenses.appcreates the scriptimage/bin/expenses. It works because the JAR records the module's main class.image/bin/expensesstarts the application using the image's ownjava. No installed JDK is involved.--list-modulesconfirms the image holds exactly three modules. The full JDK has 69.
Step 4: compare sizes.
| Size on this machine | |
|---|---|
| The full JDK 25 | 371 MB |
| The custom image | 42 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
| Point | Details |
|---|---|
| The image is platform-specific | An 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 module | jlink doesn't accept automatic modules. A dependency without module-info blocks it; build tools have plugins that work around this. |
| You own the updates | The image contains a fixed JDK version. Security fixes arrive only when you rebuild and redistribute. |
| It isn't an installer | It is a folder. To make a .dmg, .msi or .deb, use jpackage (see "Jpackage in Java: Native Installers"). |
When to Use It
| Good fit | Poor fit |
|---|---|
| Command-line tools and desktop applications sent to users | Applications deployed to a server that already has a managed JDK |
| Container images, where every megabyte counts | Projects whose dependencies aren't modular |
| Devices with little storage | Code 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
- Easy: Build the image without
--strip-debug --no-header-files --no-man-pagesand compare its size. - 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.
Key Takeaways
Was this tutorial helpful?