Wenn ich heute mit Nicht-JVM-Menschen über die JVM spreche, dann höre ich häufig zwei Kritikpunkte. Zum einen wird der hohe Arbeitsspeicherbedarf genannt, zum anderen die langsame Startzeit. Der zweite Punkt macht sich umso deutlicher bemerkbar, wenn wir nicht einen modernen Entwicklungsrechner, sondern die in Produktion häufig genutzten kleinen Instanzen betrachten.

Dass beide Punkte mit der Historie der JVM zusammenhängen, soll dabei keine Entschuldigung oder Ausrede sein, sondern ist einfach ein Zeichen für die Zeit, aus der die JVM hervorging. Vor 20 Jahren haben wir wenige, dafür aber größere Anwendungen gebaut und betrieben. Außerdem wurde primär die „Mean Time Between Failures“ optimiert und die Anwendungen liefen oft Monate am Stück. Es gab also wenig Bedarf für eine Optimierung der Startzeit.

Aber natürlich arbeiten die Menschen im OpenJDK daran, beide Punkte zu verbessern. Wie im JVM-Universum üblich, wird das sehr sorgfältig und stets mit dem Blick auf Rückwärtskompatibilität angegangen. Dadurch dauert das in Summe länger, geht aber stetig voran und sorgt bei uns Anwendungsentwickelnden für weniger Frust.

In diesem Artikel wollen wir uns deswegen die bereits gemachten Schritte und jene, die noch kommen, anschauen. Doch vorher wollen wir uns einen kurzen Überblick verschaffen, was eigentlich so alles beim Start einer Anwendung auf der JVM passiert.

Was geschieht beim Start?

Neben dem Auswerten von Programmargumenten und weiteren JVM-Optionen und dem Starten von Komponenten wie dem Garbage Collector verbringt die JVM einen Großteil des Starts damit, die für die Anwendung benötigten Klassen zu laden. Dabei folgt diese den in Kapitel 5 der JVM-Spezifikation definierten Regeln und Anforderungen. Dort ist genau spezifiziert, wie Klassen geladen, gelinkt und initialisiert werden. Das Verhalten ist somit auf allen JVMs identisch, auch wenn die Implementierungen im Detail variieren können.

Für das Laden von Klassen werden ClassLoader verwendet. Diese sind dabei hierarchisch aufgebaut, das bedeutet, jeder ClassLoader besitzt einen weiteren ClassLoader als Parent. Das Vorgehen ist dabei, dass ein ClassLoader, bevor er selbst eine Klasse lädt, seinen Parent bittet, diese zu laden. Nur wenn diese Bitte nicht erfüllt werden kann, ist es seine Aufgabe, die Klasse zu laden. An der Spitze dieser Hirarchie gibt es den Bootstrap-ClassLoader, der in Kapitel 5.3.1 definiert ist.

Beim Laden an sich geht es darum, die binäre Repräsentation einer Klasse – das, was in einer class-Datei zu finden ist – einzulesen. Anschließend wird daraus die dort definierte Klasse oder das Interface abgeleitet und in die Method Area (in der HotSpot JVM auch Metaspace genannt) geladen. Beim Start der JVM werden nicht alle Klassen automatisch geladen, sondern – ausgehend von der Hauptklasse – nur jene, die tatsächlich benötigt und referenziert werden.

Nach dem Laden muss die Klasse noch gelinkt werden. Dieser Schritt besteht wiederum aus den drei größeren Aufgaben der Verifikation, Vorbereitung (Preparation) und Auflösung (Resolution).

Bei der Verifikation überprüft die JVM die vorher geladene Klasse auf ihre strukturelle Korrektheit. Dabei gilt es, eine ganze Reihe von Dingen zu überprüfen (Kapitel 4.9.1 und 4.9.2 der JVM-Spezifikation). Zusätzlich gilt es sicherzustellen, dass es für eine finale Klasse keine Subklasse gibt und dass finale Methoden nicht überschrieben wurden. Außerdem muss sichergestellt werden, dass alle Klassen außer java.lang.Object eine direkte Elternklasse haben. Wird dabei eine nicht valide Klasse gefunden, wird ein VerifyError geworfen.

Nach der erfolgreichen Verifikation kann die Klasse vorbereitet werden. In diesem Schritt werden sämtliche statischen Felder erzeugt und auf ihren Standardwert gesetzt. So würde ein Feld mit der Deklaration static int foo = 42 nach diesem Schritt mit dem Wert 0 initialisiert sein.

Als Nächstes folgt die Auflösung. Hierbei werden die symbolischen Referenzen, die in der binären Repräsentation einer Klasse vorhanden sind, aufgelöst. Auch wenn diese drei Unteraufgaben hier nacheinander aufgeführt sind, kann die tatsächliche Reihenfolge der Ausführung abweichen.

Nach dem Linken kann die Klasse initialisiert werden. Dabei werden die deklarierten Werte von statischen Feldern oder Klassenvariablen gesetzt und statische Initializer ausgeführt. Dazu wird die automatisch vom Compiler generierte Methode <clinit> von der JVM aufgerufen. Diese gibt es jedoch nicht immer, sondern wirklich nur dann, wenn der Bedarf besteht.

Da die JVM in der Regel mehrere Hundert Klassen laden muss, dauert dieser Prozess mitunter eine Weile. Und somit ist er natürlich auch ein Kandidat, um durch Optimierungen die Startzeit deutlich zu reduzieren. Wie das gelingt, schauen wir uns im Folgenden an.

Class Data Sharing

Class Data Sharing, abgekürzt als CDS, wurde schon vor langer Zeit mit einem Update für JDK 5 in der HotSpot JVM eingeführt. Die Idee dahinter ist es, dass es eine Reihe von Klassen im JDK gibt wie die aus java.lang, die eigentlich jede Anwendung benötigt. Anstatt sie bei jedem Start zu laden, wird bei CDS einmalig ein Archiv erzeugt, in dem diese Klassen fertig geladen liegen. Beim nächsten Start wird das Archiv erkannt und die Klassen werden direkt von dort in den Arbeitsspeicher gepackt. Somit entfällt ein Schritt und die JVM startet schneller. Um dieses Feature zu aktivieren, musste es jedoch lange aktiv angeschaltet werden. Erst mit JDK 12 und JEP 341 wurde für CDS eingeführt, dass das Archiv bereits mit im JDK ausgeliefert und beim Start der JVM standardmäßig mitgeladen wird.

Zeitweise war es außerdem möglich, dass mehrere JVMs, die auf demselben System laufen, sich den Speicherbereich dieser Klassen teilen. Somit wurde auch der Speicherverbrauch etwas reduziert. Da das heute aber oft keine Rolle mehr spielt, wird dieser Aspekt nicht mehr primär unterstützt.

Application Class Data Sharing

Durch CDS wurde zwar die Startzeit reduziert, aber gerade bei großen Anwendungen ist der Anteil der Klassen aus dem JDK nur sehr gering, ein Großteil sind von uns selbst geschriebene oder vom genutzten Framework mitgebrachte Klassen. Angesichts dessen wurde mit JDK 10 und JEP 310 Application Class Data Sharing (AppCDS) eingeführt. Im Grunde handelt es sich hierbei um eine Erweiterung von CDS, um neben den JDK-eigenen auch die Klassen der Anwendung mit in das Archiv zu legen. Das geschieht allerdings nicht automatisch, sondern wir müssen das Feature selbst aktiv anschalten und benötigen zudem einen Trainingslauf. Dabei werden die geladenen Klassen erkannt und in eine Datei geschrieben. Mit einem zweiten Lauf kann aus dieser Liste dann ein Archiv mit den geladenen Klassen erzeugt werden.

Für den Trainingslauf nutzen wir die Optionen -Xshare:off und -XX:DumpLoadedClassList=./application.lst. Bei älteren JDKs muss zudem noch die Option -XX:+UseAppCDS gesetzt werden. Abhängig von der Anwendung können wir diese direkt nach dem Start wieder beenden oder vorher noch Aktionen ausführen, damit die Anwendung wirklich alle Klassen lädt. Beim Beenden der JVM schreibt diese die Liste aller geladener Klassen in die angegebene Datei (Listing 1).

# NOTE: Do not modify this file.
#
# This file is generated via the -XX:DumpLoadedClassList=<class_list_file> option
# and is used at CDS archive dump time (see -Xshare:dump).
#
java/lang/Object id: 0
java/io/Serializable id: 1
java/lang/Comparable id: 2
java/lang/CharSequence id: 3
java/lang/constant/Constable id: 4
...
org/springframework/util/ConcurrentReferenceHashMap$EntrySet id: 5293
org/springframework/util/ConcurrentReferenceHashMap$EntryIterator id: 5294
Listing 1: Liste der geladenen Klassen nach dem Trainingslauf

Diese Liste können wir nun nutzen, um mit den Optionen -Xshare:dump, -XX:SharedClassListFile=./application.lst und -XX:SharedArchiveFile=./application.jsa daraus ein statisches Archiv mit den geladenen Klassen zu erzeugen. Das Archiv kann beim Starten der Anwendung mit der Option -XX:SharedArchiveFile=./application.jsa referenziert werden. Die JVM wird nun eben nicht nur wie mit CDS die JDK-eigenen Klassen direkt in den Arbeitsspeicher legen, sondern eben auch die Klassen der Anwendung. Somit verringert sich die Startzeit der Anwendung noch einmal deutlich.

Um die Nutzung von AppCDS weiter zu vereinfachen, wurden in JDK 13 mit JEP 350 dynamische Archive eingeführt. Um ein solches zu erstellen, nutzen wir beim Trainingslauf nur noch die Option -XX:ArchiveClassesAtExit=./application.jsa. Dadurch wird das fertige Archiv direkt im Trainingslauf erzeugt. Wir sparen uns somit das Erzeugen der Klassenliste und deren separate Umwandlung in das Archiv.

Damit das alles funktioniert, gibt es allerdings zwei kleine Einschränkungen. Zum einen muss der Classpath, inklusive Reihenfolge, zwischen dem Trainingslauf und dem eigentlichen Start identisch sein, und zum zweiten muss dasselbe JDK genutzt werden. Zum Glück sind beide Bedingungen bei der Nutzung von Containern leicht umzusetzen, beispielsweise mit einem Multi-Stage Dockerfile, wie es in Listing 2 zu sehen ist.

FROM openjdk:24-slim AS builder


RUN mkdir -p /app
WORKDIR /app


COPY ./target/jvm-startup.jar jvm-startup.jar


RUN java -Djarmode=tools -jar jvm-startup.jar extract \

    --layers


FROM openjdk:24-slim


CMD [ "java", \

      "-XX:SharedArchiveFile=./application.jsa", \
      "-jar", \
      "jvm-startup.jar"]

RUN mkdir -p /app && \

    chown -R daemon /app

USER daemon

WORKDIR /app


COPY --from=builder /app/jvm-startup/dependencies/ ./

COPY --from=builder /app/jvm-startup/spring-boot-loader/ ./

COPY --from=builder /app/jvm-startup/snapshot-dependencies/ ./

COPY --from=builder /app/jvm-startup/application/ ./


RUN java \

    -Dspring.context.exit=onRefresh \
    -XX:ArchiveClassesAtExit=./application.jsa \
    -jar jvm-startup.jar
Listing 2: Multi-Stage Dockerfile für AppCDS einer Spring-Boot-Anwendung

Da es sich im Beispiel um eine Spring-Boot-Anwendung handelt, besteht der erste Trick darin, das Executable-JAR zu entpacken. Hierzu nutzen wir die im JAR enthaltenen jarmode-Tools und das Kommando extract mit der Option –layers in der ersten Stage. Dadurch entsteht die Listing 3 gezeigte Struktur.

$ tree jvm-startup
jvm-startup
├── application
│   └── jvm-startup.jar
├── dependencies
│   └── lib
│       ├── jackson-annotations-2.19.2.jar
│       ├── jackson-core-2.19.2.jar
...
│       ├── tomcat-embed-el-10.1.43.jar
│       └── tomcat-embed-websocket-10.1.43.jar
├── snapshot-dependencies
└── spring-boot-loader

6 directories, 31 files
Listing 3: Struktur der entpackten Spring-Boot-Anwendung

In der zweiten Stage kopieren wir nun die Struktur aus der ersten in unser Image und starten anschließen mit RUN den Trainingslauf, um das AppCDS-Archiv zu erzeugen. Hierzu nutzen wir die Option -Dspring.context.exit=onRefresh. Sie sorgt dafür, dass die Anwendung automatisch heruntergefahren wird, nachdem der Spring-Kontext aufgebaut und geladen wurde. An dieser Stelle sind bereits die meisten Klassen geladen und befinden sich nun erfolgreich im Archiv.

Mit JDK 19 wurde in JDK-8261455 zudem die Option -XX:+AutoCreateSharedArchive hinzugefügt. Nutzen wir diese in Verbindung mit -XX:SharedArchiveFile wird, wenn kein Archiv vorhanden ist, beim Beenden der JVM eines geschrieben, und falls ein Archiv vorhanden ist, wird es genutzt.

Project Leyden

Innerhalb des OpenJDK werden größere Bemühungen in separaten Projekten umgesetzt. Eines davon ist Leyden. Sein Ziel besteht darin, die Startzeit, die Zeit bis zur Peak-Performance und den Fußabdruck der JVM zu verringern. Wie für OpenJDK-Projekte üblich, geschieht das nicht in einem riesigen Wurf, sondern als eine Reihe kleinerer JEPs, durch die man sich Schritt für Schritt dem großen Ziel annähert.

In diesem Rahmen hat Projekt Leyden bereits im JDK 24 mit dem JEP 483 ein erstes Feature geliefert. Es baut auf den Schultern von CDS bzw. AppCDS auf und verringert die Startzeit damit erneut ein ganzes Stück. Hierzu wird nicht mehr nur die binäre Repräsentation der Klassen der Anwendung direkt in den Arbeitsspeicher geladen, sondern auch der Schritt des Linkens wird mit im Archiv abgelegt. Ähnlich wie AppCDS müssen wir hierzu allerdings auch einen Trainingslauf durchführen, um die benötigten Daten aufzuzeichnen. Hierzu wird die Anwendung mit den beiden Optionen -XX:AOTMode=record und -XX:AOTConfiguration=./application.aotconf ausgeführt und anschließend beendet.

Die so geschriebene Konfiguration müssen wir noch mit einem zweiten Aufruf in das eigentliche Archiv konvertieren. Hierzu müssen drei weitere Optionen, nämlich -XX:AOTMode=create, -XX:AOTConfiguration=./application.aotconf und -XX:AOTCache=./application.aot gesetzt werden.

Anschließend lässt sich das so erzeugte Archiv beim Start der Anwendung mit der Option -XX:AOTCache=./application.aot nutzen. In Listing 4 ist ein hierfür nutzbares Multi-Stage Dockerfile zu sehen.

FROM openjdk:24-slim AS builder


RUN mkdir -p /app
WORKDIR /app


COPY ./target/jvm-startup.jar jvm-startup.jar


RUN java -Djarmode=tools -jar jvm-startup.jar extract \

    --layers


FROM openjdk:24-slim


CMD [ "java", \

      "-XX:AOTCache=./application.aot", \
      "-jar", \
      "jvm-startup.jar"]

RUN mkdir -p /app && \

    chown -R daemon /app

USER daemon

WORKDIR /app


COPY --from=builder /app/jvm-startup/dependencies/ ./

COPY --from=builder /app/jvm-startup/spring-boot-loader/ ./

COPY --from=builder /app/jvm-startup/snapshot-dependencies/ ./

COPY --from=builder /app/jvm-startup/application/ ./


RUN java \

    -Dspring.context.exit=onRefresh \
    -XX:AOTMode=record -XX:AOTConfiguration=./application.aotconf \
    -jar jvm-startup.jar

RUN java \

    -XX:AOTMode=create -XX:AOTConfiguration=./application.aotconf
    -XX:AOTCache=./application.aot \
    -jar jvm-startup.jar
Listing 4: Multi-Stage Dockerfile für­Leyden AOT einer Spring-Boot-Anwendung mit JDK 24

In JDK 25 wird dieses Feature durch zwei weitere JEPs, 514 und 515, erweitert.

JEP 514 optimiert dabei die Benutzung des Features und die benötigten Optionen, ähnlich wie bei AppCDS. Wir brauchen damit nur noch einen Trainingslauf, um direkt das Archiv zu erzeugen. Der Zwischenschritt über die Konfiguration entfällt. Es wird nur noch die Option -XX:AOTCacheOutput=./application.aot benötigt.

Mit JEP 515 wird zusätzlich dafür gesorgt, dass während des Trainingslaufs aufgezeichnete Informationen über Methoden mit in das Archiv geschrieben werden. Hierdurch kann die JVM beim erneuten Lauf früher feststellen, welche Methoden der JIT-Compiler wie optimieren kann. Somit wird die Methode deutlich früher beispielsweise in Maschinencode übersetzt und weniger oft von der JVM selbst interpretiert – und die Anwendung erreicht deutlich schneller ihre Peak-Performance. Selbstverständlich werden trotzdem weiterhin Profilinginformationen aufgezeichnet, schließlich kann sich der reale Workload von dem des Trainingslaufes unterscheiden. Somit kann die JVM zur richtigen Laufzeit immer noch weitere Optimierungen vornehmen oder alte verwerfen.

Darüber hinaus gibt es im aktuellen Early-Access-Release von Projekt Leyden bereits weitere Features, die es in Zukunft vermutlich als JEPs mit ins JDK schaffen werden. Dazu zählt vor allem, weitere Dinge mit in das Archiv zu packen, um noch mehr Arbeit bereits vor dem eigentlichen Start der Anwendung zu erledigen. Hierzu zählt das Erzeugen von Dynamic Proxies, Informationen über Zugriffe per Reflection und sogar beim Trainingslauf bereits vom JIT kompilierten Code mit in das Archiv zu legen. Außerdem ist mit JEP 516 bereits ein JEP in Vorbereitung, das die Unterstützung der bisherigen Features mit allen Garbage Collectors sicherstellen soll.

Coordinated Restore at Checkpoint

Neben Projekt Leyden gibt es aktuell noch ein zweites Projekt, dass sich mit der Optimierung von Startzeiten beschäftigt, nämlich Projekt CRaC. Es verfolgt einen vollkommen anderen Weg, nämlich die Unterstützung sogenannter Checkpoints. Daher kommt auch der Name des Projektes: CRaC ist die Abkürzung von Coordinated Restore at Checkpoint.

Die Idee ist, dass ein Signal an eine laufende JVM-Anwendung geschickt werden kann, um einen Checkpoint zu erzeugen. Ein Checkpoint ist dabei eine Art Image oder Snapshot der laufenden Anwendung zum Zeitpunkt des Signals. Darin ist der gesamte Zustand der Anwendung vorhanden und nicht nur, wie in den vorherigen Ansätzen, Metadaten. Somit sind auch Dinge wie die Datenbankverbindungen enthalten. Das ist natürlich nicht wirklich praktisch, denn es würde bedeuten, dass ich zum Erzeugen des Checkpoints zwangsläufig bereits mit der richtigen Datenbank verbunden sein müsste. Deshalb ist eine der Aufgaben des Projektes, ein Java API zu entwickeln, mit dem die Anwendung auf das Checkpoint-Signal reagieren und Aufräumarbeiten durchführen kann. Außerdem wird dann für das Restore auch ein API benötigt, um beim Start aus so einem Checkpoint den Zustand wiederherzustellen. Der aktuelle Stand des API ist in Listing 5 zu sehen.

public class SomeResource implements Resource {

    public SomeResource() {
        Core.getGlobalContext().register(SomeResource.this);
    }

    @Override
    public void beforeCheckpoint(
            Context<? extends Resource> context) throws Exception {
        System.out.println("Before Checkpointing");
    }

    @Override
    public void afterRestore(
            Context<? extends Resource> context) throws Exception {
        System.out.println("After Checkpointing");
    }
}
Listing 5: CRaC Java API

Im Grunde besteht das zu implementierende API aus dem Interface Resource. Es enthält zwei Methoden, die es ermöglichen, Dinge beim Erstellen des Checkpoints und vor dem Wiederherstellen aus einem Snapshot auszuführen. Außerdem müssen wir die Implementierung noch im globalen Kontext registrieren, damit die Methoden an den passenden Stellen von der JVM aufgerufen werden.

Aktuell ist das Erzeugen eines solchen Snapshots noch relativ kompliziert und nicht so einfach automatisierbar. Basierend auf einer Spring-Boot-Anwendung und dem Dockerfile aus Listing 6 gelingt das mit den folgenden Schritten.

FROM bellsoft/liberica-runtime-container:jdk-21-crac-glibc AS builder


RUN mkdir -p /app
WORKDIR /app


COPY ./target/jvm-startup.jar jvm-startup.jar


RUN java -Djarmode=tools -jar jvm-startup.jar extract \

    --layers


FROM bellsoft/liberica-runtime-container:jdk-21-crac-glibc


CMD [ "java", \

      "-XX:CRaCRestoreFrom=./crac-image/", \
      "-jar", \
      "jvm-startup.jar"]

RUN mkdir -p /app && \

    chown -R daemon /app

USER daemon

WORKDIR /app


COPY --from=builder /app/jvm-startup/dependencies/ ./

COPY --from=builder /app/jvm-startup/spring-boot-loader/ ./

COPY --from=builder /app/jvm-startup/snapshot-dependencies/ ./

COPY --from=builder /app/jvm-startup/application/ ./
Listing 6: Dockerfile für CraC

Zuerst bauen wir aus dem Dockerfile mit docker build -t jvm-startup:crac-pre . ein erstes Image, das wir mit docker run --rm -it –privileged -p 8080:8080 --name crac jvm-startup:crac-prep /bin/sh starten können.

In diesem Container können wir nun den Trainingslauf mit dem Befehl java -XX:CRaCCheckpointTo=./crac-image/ -jar jvm-startup.jar starten. Nachdem wir nun die laufende Anwendung mit einigen HTTP Requests aufgewärmt haben, müssen wir das Signal für den Checkpoint an den Prozess senden. Hierzu verbinden wir uns mittels des Befehls docker exec -it crac /bin/sh mit dem laufenden Container. Hier können wir nun mit jcmd jvm-startup.jar JDK.checkpoint das Signal schicken, woraufhin die Anwendung den Snapshot erzeugt.

Damit der Snapshot mit in das finale Container-Image gelangt, nutzen wir außerhalb des Containers den Befehl docker commit crac jvm-startup:crac-run. Anschließend können wir mittels docker run --rm -p 8080:8080 jvm-startup:crac-run die Anwendung wieder starten. Da sie nun den Snapshot nutzt, startet sie nicht nur schnell, sondern der erste Request wird auch sofort beantwortet.

Aktuell gibt es nur zwei mir bekannte JVMs mit Support für CRaC, nämlich Azul Zulu und Bellsoft Liberica. Zudem gibt es in IBM Semeru mit CRIU einen Mechanismus, der identisch funktioniert. Bei allen JVMs ist für die Nutzung zudem Linux als unterliegende Plattform notwendig. Unter Windows funktioniert es bisher grundsätzlich nicht.

Sonstiges im JDK

Neben den bisher vorgestellten expliziten Mechanismen wird im JDK konstant daran gearbeitet, die Performanz zu erhöhen. So ist mir kürzlich erst aufgefallen, dass ein Microbenchmark, die ich vor etwa fünf Jahren noch auf JDK 14 geschrieben habe, um die Geschwindigkeit von toUpperCase und toLowercase zu vergleichen (Listing 7), mittlerweile deutlich schneller ist als damals.

@BenchmarkMode(AverageTime)
@OutputTimeUnit(NANOSECONDS)
@Fork(1)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(5)
@State(Thread)
public class ToUpperOrToLowerCase {

    public static void main(String[] args) throws Exception {
        final Options opt = new OptionsBuilder()
            .include(ToUpperOrToLowerCase.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }

    private String string = RandomStringUtils.randomAlphabetic(32);

    @Benchmark
    public String toLowerCase() {
        return string.toLowerCase();
    }

    @Benchmark
    public String toUpperCase() {
        return string.toUpperCase();
    }
}
Listing 7: JMH Benchmark für toLowerCase und toUpperCase

JDK 25 enthält außerdem mit JEP 502 noch ein Preview-Feature, das es uns einfacher macht, Konstanten zu definieren, die nicht bereits beim Start, sondern erst bei der ersten Nutzung initialisiert werden (Listing 8).

// Deklaration mit Initialisierung von StableValue

private final Supplier<Logger> logger = StableValue.supplier(
    () -> LoggerFactory.getLogger(MyClass.class));

// Nutzung von StableValue

Logger.get().info("Hallo");
Listing 8: StableValues aus JDK 25

Dadurch sparen wir nicht nur beim Start Zeit, wenn der Wert erst später benötigt wird; außerdem kann die JVM, da sie die Semantik dieses Konstruktes kennt, hier Optimierungen vornehmen – schließlich verhält sich ein solcher Wert nach der Initialisierung wie eine Konstante.

Und auch der hohe Arbeitsspeicherbedarf der JVM wird aktuell im Project Lilliput angegangen. Ziel ist es, die Header der Objekte von aktuell zwischen 96 und 128 Bit auf 64 Bit zu reduzieren. Dadurch verringert sich der benötigte Heap und damit auch der benötigte Arbeitsspeicher. Diese Arbeit wurde bereits mit JEP 450 in JDK 24 als experimentelles Feature zur Verfügung gestellt und wird mit JEP 519 nun in JDK 25 final in die JVM integriert werden.

Conclusion

Einer der aktuellen Hauptkritikpunkte an der JVM ist deren langsamer Start. Wir haben dabei gesehen, dass ein Großteil dieser Zeit für das Laden der Klassen verwendet wird. In der Vergangenheit wurden hier bereits mit CDS und AppCDS Features zur Verfügung gestellt, um diesen Prozess zu beschleunigen.

Und die Arbeit ist – natürlich – noch nicht beendet. In naher Zukunft werden verschiedene Features aus Projekt Leyden in die JVM Einzug halten, die – aufbauend auf den beiden vorherigen – noch mehr der bislang beim Start der JVM ausgeführten Arbeit in die Build-Zeit verlagern.

Alternativ gibt es mit CRaC ein zweites Projekt, um eine gestartete Anwendung zu pausieren und anschließend von genau diesem Stand wieder zu starten. Und auch abseits der größeren Initiativen ist den Menschen, die am OpenJDK arbeiten, stets bewusst, dass Performance ein relevanter Punkt ist. Und so werden die JVMs generell von Release zu Release schneller.