Skip to content

Latest commit

 

History

History
613 lines (467 loc) · 23.5 KB

javaspektrum-jlink.adoc

File metadata and controls

613 lines (467 loc) · 23.5 KB

Image Espresso Coffee Press

espressomaschinen zubehoer ersatzteile tamper

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.

Komponenten der JVM

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.*.

Listing 1 - Im JDK vorhandene Java Module
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".

Minimales JDK mit java.base

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).

Listing 2 - Minimales JDK mit java.base
# 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
Table 1. Tabelle 1 Weitere Optionen für jlink
Option Auswirkung

--add-modules

Namen der hinzuzufügenden Module, transitive Auflösung, ALL-MODULE-PATH für alle

--module-path

Pfad für Module (jar, jmod oder ausgepackt)

--output

Zielverzeichnis für die neue Runtime, darf noch nicht existieren

--strip-debug

Debug Symbole entfernen, inkl Zeilennummern von Stacktraces und Parameternamen

--compress=0,1,2

Kompression: 0-keine, 1-String Deduplikation, 2-komprimierte Module

--vendor-version="text"

Informationstext für die Runtime

--include-locales="en,de"

Welche Sprachversionen sollen integriert werden (benötigt Modul jdk.localedata)

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]

Voraussetzungen der Anwendung

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].

Minimalbeispiel einer klassischen Anwendung

In unserer minimalen Anwendung geben wir nur einen Gruß aus, benutzen als ausser java.base keine anderen Module.

Listing 3 - Minimalanwendung "Hello World"
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.

Listing 4 Analyse, JDK-Erstellung
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.

Table 2. Tablle 1 Größenvergleich
Optionen Größe (MB)

<keine>

41

--compress=1

34

--compress=2

28

--compress=2 --strip-debug

26

Beispiel Modulare Java Anwendung

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.

Listing 5 module-info.java
module httpEchoModule {
    requires java.logging;
    requires java.net.http;
}
Listing 6 HttpEcho.java
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).

Listing 7 - Kompilieren und Ausführen des HttpEcho Demos
# 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).

Listing 8 - Analyse und Runtime erstellen für modulare Anwendung
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.

Listing 9 - Test der erstellten Runtime für die modulare Anwendung
# 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.

Listing 10 Startskripte erstellen
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 ...

Zusätzliche Module wie JavaFX

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.

Listing 11 zusätzliche Module für JavaFX
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.

Listing 12 Maven Plugin Konfiguration
<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:

Listing 13 badass-runtime-plugin
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']
}
Listing 14 badass-jlink-plugin
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']
    }
}

JReleaser

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:

Listing 15 - JReleaser JLink Assembler
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.

Docker Multistage Build

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.

Listing 16 Dockerfile
# 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"]

Andere Tools

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).

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.

References