Image Espresso Coffee Press
Es gibt verschiedene Mittel und Wege den Fußabdruck von Java Anwendungen zu reduzieren. Native Binaries mit GraalVM, Minimierung des Speicherbedarfs mit den neueren JVMs (z.B. für Strings), bessere Datenstrukturen usw. Eine weitere Möglichkeit stellt die Anpassung der JVM selbst an die Bedürfnisse der Anwendung dar.
Das kann mit jlink
dem Java Linker erfolgen, den wir uns heute genauer anschauen wollen.
Die JVM ist traditionell nicht besonders schlank, z.B. auf meinem System ist Java 17, 298 MB gross, neben den Kommandozeilentools wie java
, javac
usw. wird der meiste Platz von JDK- und nativen Bibliotheken eingenommen.
Seit Java 9 sind mit dem "Java Modulsystem" [JEP 261] die Komponenten des JDK individuell als Plattform-Module verfügbar. Im selben Zuge wurde auch die JRE (Java Runtime Environment) als abgespeckte Variante des JDK abgeschafft.
Mit java --list-modules
können die Module angezeigt werden, hier in Listing 1 ist eine kleine Auswahl der über 70 Module zu sehen.
Neben den java.*
Modulen sind auch spezifische Module des JDK verfügbar jdk.*
.
java.base@17 java.compiler@17 java.datatransfer@17 java.desktop@17 java.instrument@17 java.logging@17 java.management@17 java.net.http@17 java.prefs@17 java.rmi@17 java.scripting@17 java.se@17 java.security.jgss@17 java.sql@17 java.xml@17 jdk.httpserver@17 jdk.jfr@17 jdk.random@17
Für das Deployment von Anwendungen als Container oder Installation auf Systemen (Server und Desktop) kann es sinnvoll sein, die JVM auf die benötigten Module zu verkleinern.
Wenn der JVM weniger Plattform-Module zur Verfügung stehen, können diese auch nicht geladen werden, und benötigen keinen Speicher für Klassen- und Modul-Metadaten.
Mit jlink
dem "Java Linker" [JEP-282] kann seit Java 9 eine Selektion von Modulen vorgenommen werden, die die Abhängikeiten der konkreten Anwendung darstellt, und diese zusammen mit den regulären Tools in ein eigenes, minimales aber eigenständiges JVM-Artefakt packt.
Minimal wird das java.base
Modul genutzt, zusätziche Module (und deren transitive Abhängikeiten) nur bei Bedarf hinzugenommen.
Das können sowohl Module aus dem JDK aber auch aus Bibliotheken oder sogar der Anwendung selbst sein. Diese werden dann in der selbst erstellten Runtime zu "Systemmodulen".
Alle Java Runtimes benötigen das java.base
Modul.
Damit können wir also direkt eine minimale Runtime erstellen, die beim Ausführen von --list-modules
auch nur dieses Modul als verfügbar anzeigt, wie in Listing 2 gezeigt (alle Beispiele mit Java 17, diese Kommandozeilensyntax ab Java 10).
# Java 17 mittels SDK-Man (funktioniert ab Java 9)
sdk use java 17-open
jlink \
--add-modules java.base \
--output ./jdk-base
./jdk-base/bin/java --list-modules
# java.base@17
Option | Auswirkung |
---|---|
|
Namen der hinzuzufügenden Module, transitive Auflösung, |
|
Pfad für Module (jar, jmod oder ausgepackt) |
|
Zielverzeichnis für die neue Runtime, darf noch nicht existieren |
|
Debug Symbole entfernen, inkl Zeilennummern von Stacktraces und Parameternamen |
|
Kompression: 0-keine, 1-String Deduplikation, 2-komprimierte Module |
|
Informationstext für die Runtime |
|
Welche Sprachversionen sollen integriert werden (benötigt Modul |
Es ist zu beachten, dass (wie auch das JDK) die erstellten Runtimes betriebssystemspezifisch sind. JLink kann aber auch Runtimes für andere Betriebssysteme erstellen, siehe [MultiOS]
Die Vorraussetzung für solch eine Selektion ist, dass die eigene Anwendung entweder schon im Modulpfad definiert ist, oder für ältere Anwendungen die Abhängigkeiten auf dem Classpath mit Tools wie jdeps
festgestellt werden können.
Wie schon bei nativen Images mittels GraalVM sind indirekte Nutzung von APIs wie z.B. mittels Reflection oder Methodhandles nicht festzustellen und müssten manuell hinzugefügt werden [ReflectionParlog].
In unserer minimalen Anwendung geben wir nur einen Gruß aus, benutzen als ausser java.base
keine anderen Module.
package de.jlink;
public class Hallo {
public static void main(String...args) {
System.out.println("Gruß JavaSpektrum!");
}
}
Bestimmung von Abhängigkeiten von nicht-modularen Java Anwendungen mit jdeps
und Erstellung des JDK mittles jlink
in Listing 4.
java Hallo.java # Gruß JavaSpektrum! javac Hallo.java jar --create --file hallo.jar --main-class Hallo Hallo.class # Analyse der Abhängigkeiten jdeps Hallo.class # Ausgabe: # Hallo.class -> java.base # de.jlink -> java.io java.base # de.jlink -> java.lang java.base # jdeps -s Kurzform (-R rekursiv) jdeps -s Hallo.class Hallo.class -> java.base # Funktioniert auch mit Jar Archiv jdeps -s hallo.jar # Ausgabe: Hallo.class -> java.base # voherigen Lauf aufräumen rm -rf hallo-jdk jlink -v --add-modules java.base \ --compress=2 --strip-debug --no-header-files \ --no-man-pages \ --output ./hallo-jdk du -sh ./hallo-jdk
In der Tabelle 1 können die Auswirkungen der verschiedenen Optionen zur Platzeinsparung nachvollzogen werden.
Optionen | Größe (MB) |
---|---|
<keine> |
41 |
--compress=1 |
34 |
--compress=2 |
28 |
--compress=2 --strip-debug |
26 |
In einer minimal komplexeren modularen Anwendung nutzen wir das java.logging
Modul und den HttpClient
aus java.net.http
seit Java 11, siehe Listings 5 und 6.
module httpEchoModule {
requires java.logging;
requires java.net.http;
}
package de.jexp.jlink;
import java.util.logging.Logger;
import java.net.http.*;
import java.net.URI;
public class HttpEcho {
private static Logger LOG = Logger.getLogger("echo");
public static void main(String...args) throws Exception {
// GET Request zu postman echo service
var request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.GET().build();
var client = HttpClient.newHttpClient();
var response = client.send(request,
HttpResponse.BodyHandlers.ofString());
LOG.info("status "+response.statusCode());
LOG.info(response.body());
}
}
Im Anschluss kompilieren wir beide Dateien auf dem Modul-Pfad und führen sie testweise aus (Listing 7).
# Kann auch zusammen kompiliert werden
javac -d target module-info.java
javac -d target --module-path target HttpEcho.java
java --module-path target --module httpEchoModule/de.jexp.jlink.HttpEcho
# Ausgabe
Jan. 23, 2022 10:21:01 PM de.jexp.jlink.HttpEcho main
INFO: status 200
Jan. 23, 2022 10:21:01 PM de.jexp.jlink.HttpEcho main
INFO: {"args":{},"headers":{"x-forwarded-proto":"https",...
Jetzt können wir wie gehabt jdeps
nutzen, um uns die genutzten System-Module unserer modularen Anwendung --module httpEchoModule
informativ anzuzeigen - dabei gibt es keine Überraschungen.
Ebenso kann jlink
unser Modul --add-modules httpEchoModule
direkt übergeben werden, die transitiven Abhängigkeiten werden automatisch ermittelt (Listing 8).
jdeps --module-path target -s --module httpEchoModule
# Ausgabe
httpEchoModule -> java.base
httpEchoModule -> java.logging
httpEchoModule -> java.net.http
# Eigene Runtime erstellen für unser Modul
jlink --module-path target \
--add-modules httpEchoModule \
--output ./echoRuntime
Im Listing 9 wird gezeigt welche Module in unserer neuen Runtime vorhanden sind, und dass unsere Anwendung darin problemlos ausgeführt werden kann. Neben den Java Plattform Modulen ist auch das Modul unserer Anwendung integriert.
# welche Module sind in der neuen Runtime vorhanden
./echoRuntime/bin/java --list-modules
hwModule
java.base@17
java.logging@17
java.net.http@17
# Ausführen unseres Moduls in der erstellten Runtime
./echoRuntime/bin/java --module httpEchoModule/de.jexp.jlink.HttpEcho
Jan 23, 2022 10:23:03 PM de.jexp.jlink.HttpEcho main
INFO: status 200
Jan 23, 2022 10:23:03 PM de.jexp.jlink.HttpEcho main
INFO: {"args":{},"headers":{"x-forwarded-proto":"https",...
Mit einem zusätzlichen Parameter --launcher
können der erstellten Runtime auch noch Startskripte für die Anwendung mitgeben werden.
In Listing 10 wird das verdeutlicht.
jlink --module-path target \
--add-modules httpEchoModule \
--launcher http-echo=httpEchoModule/de.jexp.jlink.HttpEcho \
--output ./echoRuntime
./echoRuntime/bin/http-echo
# Ausgabe
Jan 23, 2022 10:48:49 PM de.jexp.jlink.HttpEcho main
INFO: status 200 ...
Falls zusätzliche Module benötigt werden die nicht (mehr) im JDK vorhanden sind, wie z.B. JavaFX, können diese bei der Analyse der Abhängigkeiten mit angegeben werden.
JavaFX wurde nie ins OpenJDK übernommen, daher gibt es von Gluon im [OpenJFX] Projekt das [JavaFXSDK] zum Herunterladen mit den entsprechenden Modulen.
Für jdeps
und jlink
können diese Module auf dem Modul-Pfad zusätzlich mit angegeben werden wie in Listing 11 zu sehen.
jdeps --module-path $JAVAFX/javafx-sdk-11/lib \
--add-modules=javafx.controls \
--print-module-deps MyApp.jar
jlink --no-header-files --no-man-pages --compress=2 \
--strip-debug --add-modules java.desktop,\
java.logging,java.scripting,java.xml,\
jdk.jsobject,jdk.unsupported,\
jdk.unsupported.desktop,jdk.xml.dom \
—output java-runtime
Im realen Einsatz möchte man natürlich nicht jlink
ständing auf der Kommandozeile ausführen, daher gibt es entsprechende Maven, Gradle, JReleaser Plugins bzw. die Möglichkeit es in einen Docker-Deploy-Build mit zu integrieren.
Im folgenden sollen die Möglichkeiten kurz aufgezeigt werden, zuerst das [JlinkMavenPlugin].
Diese Plugin erzeugt je nach Konfiguration (Listing 12) eine betriebssystemabhängige Zip-Datei mit der Runtime, der Anwendung und entsprechenden Startskripten.
<project …>
<modelVersion>4.0.0</modelVersion>
<!-- benötigt extension=true im plugin-->
<packaging>jlink</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jlink-plugin</artifactId>
<version>3.1.0</version>
<extensions>true</extensions>
<configuration>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<launcher>http-echo=httpEchoModule/d.j.e.HttpEcho</launcher>
</configuration>
</plugin>
</plugins>
</build>
</project>
Für Gradle gibt es 2 Plugins, eines für modulbasierte Anwendungen (badass-jlink-plugin
[JlinkGradlePlugin]) und eines für den Rest (badass-runtime-plugin
).
Ihre Konfiguration (Listing 13 und 14) ist ähnlich, und hält keine Überraschungen bereit:
plugins {
id 'org.beryx.runtime' version '1.12.7'
}
...
runtime {
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
modules = ['java.naming', 'java.xml']
}
plugins {
id 'org.beryx.jlink' version '2.24.4'
}
...
jlink {
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
launcher{
name = 'hello'
jvmArgs = ['-Dlog4j.configurationFile=./log4j2.xml']
}
}
Ein Tool das in den letzten Monaten viel Aufmerksamkeit bekommen hat, und auch einen zukünftigen Artikel verdient, ist [JReleaser] von Andres Almiray.
Nach dem Vorbild von GoReleaser erlaubt es Java Anwendungen in vielfältiger Art und Weise zu publizieren. Die Paketierung wird dabei vor allem konfiguriert, JReleaser kümmert sich um die Ausführung der verschiedenen Build- und Paketierungs-Tools.
Beispiele für Release-Ziele sind:
-
Maven Central
-
Homebrew
-
RPM / Debian Packages
-
Docker Images
-
uvm.
Der JLink [Assembler] erstellt die Runtime als Teil des Buildprozesses.
Im Assembler gibt es vielfältige Konfigurationsoptionen für den Erstellungsprozess inklusive Bereitstellung verschiedener JDK-Versionen für Betriebssysteme, Argumente für jdeps
und jlink
sowie Benennung und Strukturierung von Artefakten.
Hier ein Beispiel für eine Konfiguration:
assemble:
jlink:
app:
active: always
mainJar:
path: 'target/{{distributionName}}-{{projectVersion}}.jar'
jdk:
path: /home/jdks/16.0.0-zulu-osx
platform: osx
targetJdks:
- path: /home/jdks/16.0.0-zulu-linux_x64
platform: linux
JReleaser unterstützt auch [JLink Distributionen] mit eigenen JDK Runtimes, die mittels jlink
erstellt wurden.
Dann wird keine JVM Installation auf dem Zielsystem vorgenommen bzw. vorausgesetzt.
Da angepasste Runtimes betriebssystemspezifisch sind, müssen sie für verschiedene Zielsysteme bereitgestellt werden und als Zip-Datei in die Distribution integriert werden.
Besonders auf Betriebssystemen auf denen die notwendigen Linux-JDK-Distributionen nicht so einfach zu handhaben sind und für Continuous-Integration-Systeme, sind auch multi-stage Docker-Builds eine Variante.
Dabei werden in einer einzigen Dockerfile
Datei mehrere Builds definiert die aufeinander aufbauen können.
In Listing 16 ist zu sehen, wie eine eigene Runtime in einem openjdk Docker Image erzeugt und dann auf ein Linux-Image installiert wird.
# Multi-stage Docker build
FROM openjdk:17.0.2 as runtime-build
# 1. Mittels Jlink eigenes JDK in "/custom-jdk" bauen
RUN $JAVA_HOME/bin/jlink \
--add-modules java.base \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /custom-jdk
# Stage 2. Custom JDK in Debian JVM installieren
FROM debian:buster-slim
ENV LANG en_US.UTF-8
ENV JAVA_HOME /usr/lib/jvm/custom-jdk
ENV PATH "${JAVA_HOME}/bin:${PATH}"
# Kopieren des vorher gebauten JDKs
COPY --from=runtime-build /custom-jdk $JAVA_HOME
# Anwendung kopieren und mittels des neuen JDK ausführen
RUN mkdir /opt/app
COPY hello.jar /opt/app
CMD ["java", "-jar", "/opt/app/hello.jar"]
Eine nützliche Idee ist [jlink.online], ein AdoptOpenJDK Service, der dynamisch via API Aufruf neue JDKs baut und ausliefert.
So kann man mit einem curl
Aufruf sich das Binary für ein eigenes JDK herunterladen, z.B. mittels https://jlink.online/runtime/x64/linux/11.0.8+10?modules=java.desktop,jdk.zipfs
Damit kann auch der "multi-stage" Docker Build eingespart werden, und das notwendige JDK einfach beim Erstellen des Docker Images nach Bedarf hinzugefügt werden (siehe Listing 17).
FROM alpine:latest
RUN apk add curl
# custom JDK herunterladen und auspacken
RUN curl -G 'https://jlink.online/runtime/x64/linux/17.0.2' \
-d modules=java.base \
| tar zxf -
Gunnar [Morling] hat einen interessanten Artikel zur Paketierung von Quarkus Anwendungen mit JLink und AppCDS (Class Data Sharing) verfasst, den ich empfehlen kann.
-
[JEP282] https://openjdk.java.net/jeps/282
-
[JlinkDocs]: https://dev.java/learn/jlink---assemble-and-optimize-a-set-of-modules/
-
[Morling] https://www.morling.dev/blog/smaller-faster-starting-container-images-with-jlink-and-appcds/
-
[dev.java] https://dev.java/learn/creating-runtime-and-application-images-with-jlink/
-
[MBien] https://mbien.dev/blog/entry/custom-java-runtimes-with-jlink
-
[Baeldung] https://www.baeldung.com/jlink
-
[OpenJFX] https://wiki.openjdk.java.net/display/OpenJFX/Main
-
[JavaFXSDK] https://gluonhq.com/products/javafx/
-
[jlink.online] https://github.com/AdoptOpenJDK/jlink.online https://mbien.dev/blog/entry/custom-java-runtimes-with-jlink
-
[JlinkMavenPlugin] https://maven.apache.org/plugins/maven-jlink-plugin/
-
[JlinkGradlePlugin] https://badass-jlink-plugin.beryx.org/releases/latest/
-
[Assembler] https://jreleaser.org/guide/latest/configuration/assemble/jlink.html
-
[JReleaser] https://jreleaser.org
-
[JLink Distributionen] https://jreleaser.org/guide/latest/distributions/jlink.html
-
[ReflectionParlog] https://stackoverflow.com/questions/70664036/find-the-missing-module/70733470#70733470
-
[MultiOS] https://dev.java/learn/creating-runtime-and-application-images-with-jlink/#cross-os