Obwohl schon häufig für tot erklärt, erfreut sich Java und die Entwicklung von Java-Anwendungen weiterhin großer Beliebtheit. Java-seitig hat sich in den letzten Jahren mit Java 21 und 25 so einiges getan. Die Sprache wurde durch viele neue und überarbeitete Features umfassend modernisiert, so dass sie sich nicht hinter vielen moderneren Programmiersprachen zu verstecken braucht.
Auch die Performance und der Speicherverbrauch wurden von Release zu Release immer weiter optimiert.
Nichtsdestotrotz ist die Performance beim Starten einer Java-Anwendung immer noch nicht vergleichbar
mit nativen Anwendungen. Und auch der Speicherverbrauch ist meistens deutlich höher.
Können wir als Entwickler irgendetwas daran ändern? In gewissen Grenzen ja, aber es bleibt trotzdem
eine JVM basierte Programmiersprache mit einem gewissen systembedingten Overhead. Kann man also
nichts machen? Doch, denn das ursprünglich aus den Oracle Labs stammende und weiterhin von Oracle
unterstützte Projekt GraalVM bietet die Möglichkeit Java-Anwendungen mittels einer ahead-of-time
(AOT) Kompilierung zu einem nativen Executable zu kompilieren.
Mit der aktuellen Version GraalVM 25 ist es ein vollständiges JDK mit allen zur nativen Kompilierung
notwendigen Utilities und Tools. Und das habe ich ausprobiert, um zu schauen, wie gut es funktioniert
und was es bringt. Dazu habe ich eine einfache Spring Boot / Thymeleaf Web Anwendung entwickelt.
Und dann geschaut, wie groß die erzeugten Artefakte (jar-File, Executable, Docker-Image) sind, wie lang
die Startzeiten sind, wie viel Speicher zur Laufzeit benötigt wird und wie die Antwortzeiten aussehen.
Hier erst einmal die nackten Zahlen, die auf einem 16GB / Ryzen 7 Pro 4750U Notebook unter Linux
Mint ermittelt wurden.
Klassisch kompiliert zu einem jar-File:
- Erzeugtes jar-File: ~24 MB
- Startzeit der Anwendung mittel "java -jar": ~2,524 Sekunden
- Speicherverbrauch: 194-366 MB, im Durchschnitt ~226 MB
- Antwortzeiten (laut Browser): 10-20 ms, im Durchschnitt ~16 ms
- Größe des Docker-Image: 215 MB
Mit GraalVM zu einem nativen Executable kompiliert:
- Erzeugtes natives Executable: ~92 MB
- Startzeit der nativen Anwendung: ~0,049 Sekunden
- Speicherverbrauch: 78-87 MB, im Durchschnitt ~87 MB
- Antwortzeiten (laut Browser): 5-7 ms, im Durchschnitt ~6 ms
- Größe des Docker-Image: 127 MB
Vorteile
Startzeit
Wie man deutlich sehen kann, wird die Startzeit von 2,524 auf 0,049 Sekunden massiv verkürzt. Das mag in Fällen, wo eine Anwendung gestartet wird und dann lange läuft, nicht so sehr ins Gewicht fallen. In anderen Fällen, wie z.B. bei in der Cloud ausgeführten Lambda Functions, die häufig gestartet werden, ist das ein wichtiger Faktor. Denn hier zahlt man für die tatsächlichen Start- und Laufzeiten.
Speicherverbrauch
Ähnliches gilt auch für den Speicherverbrauch. Je geringer der Speicherverbrauch, desto mehr Anwendungen kann man auf einer Maschine laufen lassen. Und bei meiner Anwendung benötigt die kompilierte Version ~61% weniger Speichers zur Laufzeit. Insgesamt spart das in der Cloud, aber auch On-Premises, Kosten ein.
Antwortzeiten / Verarbeitungsgeschwindigkeit
Wie man auch sehen kann, sind die Antwortzeiten der nativ kompilierten Version, um ca. den Faktor 2,5 schneller. Das ist meine Erfahrung nach auch bei Standard (also nicht Spring Boot) Java-Anwendungen der Fall. D.h. der Code wird ca. 2-2,5x schneller ausgeführt. Wie schon weiter oben erwähnt, ist das z.B. in Cloud-Bereich nicht ganz unwichtig. Und schnelle bzw. responsive Anwendungen mag ja auch jeder. Das sind schon wirklich handfeste Vorteile. Aber wie das im Leben immer so ist, keine Vorteile ohne Nachteile.
Nachteile
Plattformunabhängigkeit... bye, bye
Der erste Nachteil ist, dass die nativ kompilierten Java-Anwendungen nur auf der Zielarchitektur laufen, für die sie kompiliert wurden. Die Plattformunabhängigkeit ("Write once, run anywhere") von zu Bytecode kompilierten Java-Anwendungen ist damit nicht mehr gegeben.
Höherer Aufwand
Der zweite Nachteil ist, dass nur einfachste Java-Anwendung out-of-the-box nativ kompilierbar sind. Der ahead-of-time (AOT) hat die Anforderung, dass die gesamte Anwendung und deren Abhängigkeiten zur Kompilierzeit bekannt sein müssen. Bei größeren und komplexeren Java-Anwendungen, die z.B. Serialisierung durchführen, mit Reflection arbeiten etc., muss der Entwickler dem GraalVM Compiler diese Zusatzinformationen mitteilen. Der GraalVM Compiler ist nämlich nicht in der Lage diese Informationen selbst zu ermitteln.
Erst einmal Kaffee holen...
Der dritte Nachteil ist, dass der Kompilierungsvorgang mehrere Minuten in Anspruch nehmen kann. Und das bei einer Java-Anwendung, deren klassische Kompilierung zu Bytecode nur wenige Sekunden dauert. Das ist also ein Vorgang, den man nicht ständig nach jeder Codeanpassung durchführen will oder sollte.
Fazit
Zu der GraalVM Technologie gibt es noch einiges (z.B. Polyglot Capabilities) zu sagen, aber ich belasse es fürs Erste hiermit. Die GraalVM Technologie hält, was sie verspricht. Schlankere, schneller startende und schneller ausführbare Applikationen. Für wenn das interessant klingt, dem kann ich nur empfehlen, auf der GraalVM Webseite vorbeizuschauen und die Technologie auszuprobieren. Meiner Meinung nach lohnt es sich.
Habt Ihr die GraalVM schon eingesetzt? Welche Erfahrungen habt ihr gemacht und wie ist eure Meinung zu dem Thema?
Kommentare