jlink (Java 9+) creates custom runtime images containing only the modules your application needs. jpackage (Java 14+) packages applications into native installers.

Reduce deployment size by including only required JDK modules instead of the full JDK (~300 MB → ~40 MB).

Basic Usage

  # Compile modular application
javac -d out --module-source-path src $(find src -name "*.java")

# Create custom runtime
jlink --module-path $JAVA_HOME/jmods:out \
      --add-modules com.example.myapp \
      --launcher myapp=com.example.myapp/com.example.Main \
      --output myapp-runtime \
      --strip-debug \
      --compress=2 \
      --no-header-files \
      --no-man-pages
  

Run:

  myapp-runtime/bin/myapp
  

Finding Required Modules

  # Analyze dependencies
jdeps --print-module-deps --ignore-not-found --multi-release 21 myapp.jar
# Output: java.base,java.logging,java.sql,java.naming,...

jlink --module-path $JAVA_HOME/jmods:myapp.jar \
      --add-modules $(jdeps --print-module-deps myapp.jar) \
      --output myapp-runtime
  
Option Purpose
--strip-debug Remove debug info — smaller image
--compress=2 Compress resources (0=none, 2=zip)
--no-header-files Exclude C header files
--no-man-pages Exclude man pages
--launcher name=module/class Add executable launcher
--vm server|client|minimal JVM variant

jpackage — Native Installers

Creates platform-native packages (.msi, .dmg, .deb, .rpm):

Requirements

  • Custom runtime from jlink (or full JDK)
  • Application JAR or modular app

Basic Usage

  jpackage --input lib \
         --name MyApp \
         --main-jar myapp.jar \
         --main-class com.example.Main \
         --runtime-image myapp-runtime \
         --type dmg \
         --app-version 1.0.0 \
         --vendor "Example Inc" \
         --description "My Java Application"
  

Output by Platform

Platform Format Flag
macOS .dmg or .pkg --type dmg
Windows .msi or .exe --type msi
Linux .deb or .rpm --type deb

With Custom Icon

  jpackage --input lib \
         --name MyApp \
         --main-jar myapp.jar \
         --runtime-image myapp-runtime \
         --icon myicon.icns \
         --type dmg
  

End-to-End Example

  # 1. Build JAR
./mvnw package -DskipTests

# 2. Determine modules
DEPS=$(jdeps --print-module-deps target/myapp.jar)

# 3. Create custom runtime
jlink --module-path $JAVA_HOME/jmods:target/myapp.jar \
      --add-modules $DEPS \
      --strip-debug --compress=2 \
      --no-header-files --no-man-pages \
      --launcher myapp=org.springframework.boot.loader.JarLauncher \
      --output target/runtime

# 4. Create installer
jpackage --input target \
         --name MyApp \
         --main-jar myapp.jar \
         --runtime-image target/runtime \
         --type dmg \
         --app-version 1.0.0
  

GraalVM Native Image (Alternative)

For even smaller, faster-starting deployments, GraalVM Native Image compiles to a standalone native binary:

  native-image -jar myapp.jar myapp
./myapp   # no JVM needed
  

Trade-offs: longer build time, reflection configuration required, no dynamic class loading.

Comparison

Approach Size Startup Dynamic Features
Full JDK + JAR ~300 MB Slow Full
jlink runtime ~40-80 MB Medium Full
GraalVM native ~50-100 MB Fast Limited

Best Practices

  • Always use jlink for production deployments — avoid shipping the full JDK
  • Run jdeps to determine exact module dependencies
  • Test the custom runtime thoroughly — missing modules cause NoClassDefFoundError
  • Use jpackage for user-facing desktop applications
  • Consider GraalVM Native Image for CLI tools and serverless where startup time matters