jar, jlink, jpackage
from a jar to a tiny custom runtime to a native installer.
- chapter
- chapter 12.04
- reading time
- 8 min read
three steps from your code to someone else's machine. a jar is a zip of class files plus a manifest. jlink builds a custom runtime that contains only the JDK modules you use (a hello world needs 45 MB instead of 293). jpackage wraps that runtime and your app into a real installer: .dmg, .msi, .deb.
each step removes one thing the user has to install themselves.
the picture
think of sending someone a meal. a jar is the recipe and the ingredients: useful only if they already own a kitchen (a JDK). jlink packs a tiny kitchen with only the pans this recipe needs. jpackage puts the kitchen and the ingredients in one box with a big "open me" label on it, in the shape their country's front door expects.
hover a stage.
what's in a jar
a jar is literally a zip file. unzip -l works on it. the only special part is META-INF/MANIFEST.MF, a text file the launcher reads.
$ jar --create --file hello.jar --main-class Hello -C out .
$ jar --list --file hello.jar
META-INF/
META-INF/MANIFEST.MF
Hello.class
$ unzip -p hello.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 25.0.4.1 (Eclipse Adoptium)
Main-Class: Hello
$ java -jar hello.jar
hello, worldMain-Class is what makes java -jar work. a manifest can also carry Class-Path (other jars to load), Automatic-Module-Name (from the last chapter) and Multi-Release.
multi-release jars
a library that wants to support java 8 users and use List.getFirst() (java 21) on newer JVMs can ship both versions of a class in one jar (JEP 238, java 9). the JVM picks the highest version that's not newer than itself.
- ├──
- │ ├── MANIFEST.MFMulti-Release: true
- │ └──
- │ └──
- │ └── com/acme/Greeter.classxs.getFirst(), used on java 21+
- └──
- ├── Greeter.classxs.get(0), for java 8 to 20
- └── Main.class
$ jar --create --file greeter.jar --main-class com.acme.Main \
-C base . --release 21 -C v21 .
$ java -jar greeter.jar # java 25
hi (the java 21 way)
$ unzip -q greeter.jar -d x && java -cp x com.acme.Main
hi (the java 8 way)that second run is the gotcha. versioned classes only work inside a jar. unzip it onto a plain directory class path and META-INF/versions is just a folder nobody looks in.
jlink: a runtime made to measure
a full JDK is 293 MB on disk: compiler, javadoc, debuggers, 69 modules. a server running your app needs none of the tools and maybe five of the modules. jlink (JEP 282, java 9) copies just those modules into a new runtime folder. the recipe is two commands:
$ jdeps --print-module-deps --ignore-missing-deps app.jar
java.base,java.net.http,java.sql
$ jlink --add-modules java.base,java.net.http,java.sql \
--strip-debug --no-header-files --no-man-pages \
--output runtime
$ runtime/bin/java -jar app.jar$ jlink --add-modules java.base \
--strip-debug --no-header-files --no-man-pages \
--output rtno CDS archive: the full JDK ships pre-parsed classes (lib/server/*.jsa), a jlinked runtime doesn't unless you ask. that's the extra ~15 ms.
so the flags aren't free wins:
--no-header-files,--no-man-pages: pure savings, nobody needs C headers or man pages on a server.--strip-debugsaves another 4.5 MB, and it isn't free: it removes the line number tables from the JDK's own classes. your code keeps its line numbers, but every JDK frame in a stack trace turns intoUnknown Source(see below). usually fine, sometimes annoying at 3am.--compress zip-6shrinkslib/modulesfrom 25 MB to 11 MB but costs 10 to 15 ms of startup, because classes get inflated as they load.zip-9saved another 22 KB. not worth thinking about.--generate-cds-archiveadds ~28 MB of pre-parsed classes (two archives, with and without compressed oops) and wins all the startup back. the full JDK ships with these archives already, which is why a plain jlinked runtime starts slower than the JDK it came from. kinda surprising the first time.
# jlink --strip-debug
at java.base/java.util.ImmutableCollections$List12.get(Unknown Source)
at Boom.main(Boom.java:3)
# jlink without it
at java.base/java.util.ImmutableCollections$List12.get(ImmutableCollections.java:612)
at Boom.main(Boom.java:3)jpackage: a real installer
a runtime folder still needs someone to type runtime/bin/java -jar app.jar. jpackage (JEP 392, final in java 16) runs jlink for you, adds a native launcher, an icon and metadata, and builds the installer format each OS expects:
| built on | --type | what you get |
|---|---|---|
| macOS | app-image, dmg, pkg | Hello.app, a disk image, an installer package |
| windows | app-image, exe, msi | needs the WiX toolset for exe/msi |
| linux | app-image, deb, rpm | needs dpkg/rpmbuild on that machine |
$ jpackage --type app-image --name Hello --input input --main-jar hello.jar \
--add-modules java.base \
--jlink-options "--strip-debug --no-header-files --no-man-pages --strip-native-commands" \
--dest out
$ out/Hello.app/Contents/MacOS/Hello small
hello, smallthat Hello.app is 44.6 MB. leave out --add-modules and jpackage plays it safe with a default set of 52 modules: 122.9 MB for the same hello world. tell it what you need.
fat jars, jlink, native images: which one?+
a fat jar (all dependencies unzipped into one jar, what spring boot and the shade plugin produce) is the most common way to ship a server: one file, any JDK. jlink is for when you control the machine and want a small, pinned runtime, often as a container image layer. jpackage is for desktop apps and tools that end users install. and if startup time or memory really matter, GraalVM native image compiles the whole app ahead of time into one native executable, with tradeoffs (no dynamic class loading without config, slower peak throughput for some workloads). the JDK's own answer to startup is project Leyden's AOT cache, see AOT and Leyden.
- a jar is a zip with a manifest.
Main-Classmakes it runnable. - multi-release jars pick per-JVM class versions, but only when run from the jar.
- jdeps finds the modules, jlink builds the runtime. 45 MB for hello world.
- compressing a runtime costs startup. a CDS archive buys it back.
- jpackage = jlink + launcher + installer, built on the OS you're targeting.