Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 17 – Modules, packaging and distribution

jlink: custom Java runtime images

Find the modules a Java app needs with jdeps, link a runtime image with jlink, measure it against the full JDK 25, and ship the app with its own runtime.

  • Advanced
  • 22 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 22 minutes free
Free: Find the modules with jdeps · Link an image and run it · Options you will use · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 15 minutes.

What you will learn

  • Find the modules an application needs with jdeps --print-module-deps
  • Build a minimal runtime image with jlink and measure its size
  • Package an application with its own runtime image

Before you start

jlink builds a runtime image: a folder with a java command and only the modules a program needs, instead of the whole JDK. You give it the program’s modules with --module-path, name the root modules with --add-modules, and it copies those modules and every module they require into the folder named by --output. jdeps --print-module-deps lists the JDK modules a program uses, which is the list jlink needs. The image runs on a computer without any JDK, so a program can ship with exactly the Java it was tested on, and a container or installer gets smaller. jlink arrived with Java 9 (JEP 282). The JDK used here, Eclipse Temurin 25.0.4.1 for macOS on Apple silicon, ships without JMOD files, so its jlink copies modules out of the JDK’s own run-time image, a capability that JEP 493 added and that each JDK build may enable or not.

Find the modules with jdeps

The program is the shop’s billing app from the module lesson, kirana.app and kirana.bill, with one new feature: --xml exports the bill as XML for the accountant, with the javax.xml.stream API of the java.xml module.

app/src/module-info

app/src/module-info.java · app/src/module-info.java

/** The shop's program: it prints a bill, or exports it as XML for the accountant. */
module kirana.app {
    requires kirana.bill;
    requires java.xml;
}

app/src/com/example/kirana/app/Main.java

package com.example.kirana.app;

import com.example.kirana.bill.Bill;
import java.io.StringWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

/** Prints the bill; --xml prints it as XML; --split N divides the total between N people. */
public class Main {
    public static void main(String[] args) throws XMLStreamException {
        Bill bill = new Bill("Sharma Kirana Store")
                .add("Basmati rice, 25 kg", 12, 2_450_00)
                .add("Toor dal, 30 kg", 4, 4_875_00)
                .add("Sugar, 50 kg", 10, 2_250_00);
        String option = args.length > 0 ? args[0] : "";
        switch (option) {
            case "--xml" -> System.out.println(xml(bill));
            case "--split" -> System.out.println("Each pays " + bill.totalPaise() / Integer.parseInt(args[1]) + " paise");
            default -> System.out.println(bill.lines().size() + " lines, " + bill.total());
        }
    }

    static String xml(Bill bill) throws XMLStreamException {
        StringWriter out = new StringWriter();
        XMLStreamWriter xml = XMLOutputFactory.newFactory().createXMLStreamWriter(out);
        xml.writeStartElement("bill");
        for (Bill.Line line : bill.lines()) {
            xml.writeStartElement("line");
            xml.writeAttribute("quantity", Integer.toString(line.quantity()));
            xml.writeAttribute("paise", Long.toString(line.paise()));
            xml.writeCharacters(line.item());
            xml.writeEndElement();
        }
        xml.writeEndElement();
        xml.close();
        return out.toString();
    }
}
What jdeps reports, and how this JDK links Python · find_session.py
from session import run

# The two modules are compiled and packed as modular JARs, then jdeps lists the modules the application needs, and
# jlink says how this JDK links.
run(
    """
javac -d out/kirana.bill $(find bill/src -name "*.java")
javac -d out/kirana.app --module-path out $(find app/src -name "*.java")
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
jdeps --module-path mods --print-module-deps --module kirana.app
jdeps -summary --module-path mods --module kirana.app
jlink --help | tail -n 2
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d out/kirana.bill $(find bill/src -name "*.java")
$ javac -d out/kirana.app --module-path out $(find app/src -name "*.java")
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
$ jdeps --module-path mods --print-module-deps --module kirana.app
java.base,java.xml,kirana.bill
$ jdeps -summary --module-path mods --module kirana.app
kirana.app -> java.base
kirana.app -> java.xml
kirana.app -> kirana.bill
$ jlink --help | tail -n 2
Capabilities:
      Linking from run-time image enabled

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 find_session.py

  • jdeps reads the class files. It found java.xml because Main uses XMLStreamWriter, and kirana.bill because the declaration requires it; -summary shows the same edges one per line.
  • The list is the input for jlink. For modular code, --add-modules kirana.app is enough, because jlink follows the requires directives itself; for a plain JAR, the list goes straight into --add-modules.
  • This jlink links from the run-time image. Its help ends with the capability line that JEP 493 describes. A JDK that does have a jmods folder uses those files instead, and its help says “disabled” there.

jdeps has one blind spot to keep in mind from the start: it reports what the class files name, so a module that code only finds at run time, such as a provider of locale data, is missing from its list even when the program needs it.

jdeps lists the modules the app needs; jlink copies them, with java.base and java.xml from the JDK, into an image folder.mods/kirana.app.jarkirana.bill.jarjdeps --print-module-depsjava.base,java.xml,kirana.billjlink --add-modules kirana.apptakes java.base and java.xmlfrom the JDK's run-time image(this JDK has no jmods)image/bin/java, bin/kiranalib/modulesrelease: 4 moduleswhich modules?link them61.2 MiB,not 293.3 MiB

How the lesson's runtime image is made

Text description of the diagram

The diagram shows how the lesson builds a runtime image, from top to bottom.

  1. The application is two modular JARs in a folder called mods: kirana.app.jar and kirana.bill.jar.
  2. jdeps --print-module-deps reads them and lists the modules they need: java.base, java.xml and kirana.bill.
  3. jlink is given --add-modules kirana.app and a launcher called kirana. It takes the application's modules from mods, and java.base and java.xml from the JDK. This JDK has no jmods folder, so jlink copies the JDK's modules from its own run-time image.
  4. The result is a folder called image: bin/java and the bin/kirana launcher, lib/modules with the four modules, and a release file that lists them. It measured 61.2 MiB, against 293.3 MiB for the whole JDK.
A runtime image with a launcher, its contents and its size Python · link_session.py
from session import run

# A runtime image of the application and the JDK modules it needs, with a launcher; then what is in it, and how big
# it is next to the whole JDK.
run(
    """
javac -d out/kirana.bill $(find bill/src -name "*.java")
javac -d out/kirana.app --module-path out $(find app/src -name "*.java")
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
jlink --module-path mods --add-modules kirana.app --launcher kirana=kirana.app --output image
image/bin/java --list-modules
image/bin/kirana
image/bin/kirana --xml
ls image image/bin
cat image/release
java Size.java "$JAVA_HOME" image
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d out/kirana.bill $(find bill/src -name "*.java")
$ javac -d out/kirana.app --module-path out $(find app/src -name "*.java")
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
$ jlink --module-path mods --add-modules kirana.app --launcher kirana=kirana.app --output image
$ image/bin/java --list-modules
java.base@25.0.4.1
java.xml@25.0.4.1
kirana.app
kirana.bill
$ image/bin/kirana
3 lines, Sharma Kirana Store: ₹71,400.00
$ image/bin/kirana --xml
<bill><line quantity="12" paise="245000">Basmati rice, 25 kg</line><line quantity="4" paise="487500">Toor dal, 30 kg</line><line quantity="10" paise="225000">Sugar, 50 kg</line></bill>
$ ls image image/bin
image:
bin
conf
include
legal
lib
man
release

image/bin:
java
keytool
kirana
$ cat image/release
JAVA_VERSION="25.0.4.1"
MODULES="java.base java.xml kirana.bill kirana.app"
$ java Size.java "$JAVA_HOME" image
temurin-25.0.4+101.0.LTS      293.3 MiB
image                          61.2 MiB

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 link_session.py

  • Four modules, no more. --list-modules and the release file both name java.base, java.xml and the two modules of the app: everything else of the JDK’s 69 modules stayed out.
  • A launcher for the app. --launcher kirana=kirana.app wrote bin/kirana, which starts the main class that the modular JAR records, so users of the image never type a module path.
  • About a fifth of the JDK. On this build, the image measured 61.2 MiB and the whole JDK 293.3 MiB, by the sizes of their files.

A program that is not modular uses an image the same way: link the JDK modules from jdeps’ list, then start the program with the image’s own java, as image/bin/java -jar app.jar. The image is not a JDK: it has no javac and no jlink, only what the four modules contain. The jlink documentation adds a reminder that matters in production: developers are responsible for updating their custom runtime images, so every security update of the JDK means linking the image again.

Options you will use

Option What it does
--module-path mods Where the application’s modules are; the JDK’s own modules are found without it
--add-modules kirana.app The root modules; everything they require is added too
--output image The folder to create; it must not exist yet
--launcher kirana=kirana.app Writes a start script in bin for a module, or for a module and a main class
--strip-debug Removes debug information, such as the line number tables of every class
--no-header-files, --no-man-pages Leave out the C header files and the manual pages
--compress zip-6 Compresses the classes and resources inside lib/modules, from zip-0 to zip-9
--include-locales en,hi Keeps only these languages’ data in jdk.localedata, when that module is added
--bind-services Also adds every module that provides a service the image’s modules use
--generate-cds-archive Writes a class-data sharing archive, which helps the image start faster
--suggest-providers Lists the modules that provide a given service, without linking anything

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 jlink --module-path mods --add-modules kirana.app --output image. Which modules end up in the image?

    Choose one answer.

    Show the answer to question 1

    Answer: kirana.app and every module it requires, directly or through other modules, such as java.xml and java.base

    jlink resolves the root modules named by --add-modules and adds everything they require. The lesson's image held exactly four modules: java.base, java.xml, kirana.bill and kirana.app.

  2. Question 2 of 5 For a plain JAR on the class path, what does jdeps --print-module-deps app.jar give you?

    Choose one answer.

    Show the answer to question 2

    Answer: A comma-separated list of the JDK modules the JAR's classes refer to, ready for jlink --add-modules

    jdeps reads the class files and reports the modules they name. Modules found only at run time, such as service providers, are not in its list.

  3. Question 3 of 5 In the lesson, how many MiB did the runtime image of the billing app measure, without any size option?

    Type a number.

    Show the answer to question 3

    Answer: 61.2 MiB (anything from 61.1 to 61.3 counts)

    The link session measured 61.2 MiB for the image and 293.3 MiB for the whole JDK 25, by the sizes of their files, on Temurin 25.0.4.1 for macOS on Apple silicon.

  4. Question 4 of 5 What does --launcher kirana=kirana.app add to the image?

    Choose one answer.

    Show the answer to question 4

    Answer: A start script bin/kirana that runs the main class of the module kirana.app

    The launcher is a small script in bin that starts the module, with the main class given after a slash or the one its modular JAR records.

  5. Question 5 of 5 Can you compile Java code with an image built from java.base and java.xml?

    Choose one answer.

    Show the answer to question 5

    Answer: No: the image holds only the modules linked into it, and javac is in jdk.compiler

    A runtime image is not a JDK. It contains the modules you linked and nothing else, which is why it is small.

What each jlink option saves and costs, measured Pro: opens with a Pro, Premium or Ultimate pass

Every option that makes an image smaller removes something, and two of them remove things people miss. This part measures what each option saves on the same application and reproduces what the two costly ones take away.

In this part:

  • The same image five ways, measured
  • What –strip-debug costs: line numbers in stack traces
  • Start-up, and the class-data sharing archive an image lacks
  • A starting set of options for a release

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Modules jdeps cannot see: locale data and service providers, reproduced Pro: opens with a Pro, Premium or Ultimate pass

jdeps reads class files, so it reports what code refers to by name, and nothing that is found only at run time. This part reproduces the most common such gap, locale data, shows what it costs to close it, and explains why the blunt fix fails on this JDK.

In this part:

  • Month names in Hindi and Tamil, lost on a small image
  • Character sets: the same gap in another module
  • Why –bind-services fails here

2 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Linking without JMOD files: the limits, and the smallest-image exercise Pro: opens with a Pro, Premium or Ultimate pass

A JDK that links from its own run-time image is a quarter smaller, and the price is a set of things it refuses to do. This part reproduces two refusals, names the third, and ends with an exercise that brings the lesson’s measurements together.

In this part:

  • Two refusals, reproduced
  • Exercise: the smallest image for a notice
  • Rubric

3 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Key takeaways

  • jlink copies a set of modules, and everything they require, into a runtime image that runs without a JDK.
  • jdeps --print-module-deps lists the JDK modules a program refers to; jlink takes that list.
  • The lesson’s image measured 61.2 MiB against 293.3 MiB for the whole JDK.
  • An image is not a JDK, and it needs linking again for every JDK security update.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.