GraalVM & Native Images in der Theorie

von Jonas Holtkamp und Thomas Engel | 2. Dezember 2021 | Allgemein, Deutsch, Software Engineering

Jonas Holtkamp

Lead Developer

Thomas Engel

Senior Developer

GraalVM is a high-performance runtime that provides significant improvements in application performance and efficiency which is ideal for microservices.

Diesen Satz liest man als erstes auf der GraalVM-Webseite unter dem Menüpunkt “Why GraalVM?”. Aber hält sie auch alles was uns versprochen wird?

Um das herauszufinden, haben wir im Frühjahr 2021 ein Innovation Lab gestartet. In Innovation Labs bietet Senacor uns Entwickler:innen die Möglichkeit, losgelöst von Projektaufgaben neue und spannende Technologien auszuprobieren, gerade dann wenn sie im Projektalltag eher wenig Aufmerksamkeit bekommen. Wir können uns dank dieses Formats für zwei bis drei Wochen am Stück oder auch über Monate hinweg zu 100% auf diese Themen zu konzentrieren.
Diese Zeit haben wir (Thomas, Simon und Jonas) genutzt, um der Behauptung zu GraalVM einmal etwas auf den Zahn zu fühlen und zu sehen, wie viel wirklich hinter der Aussage “[GraalVM] make[s] Java applications run faster with a new just-in-time compilation technology” steckt.

Bevor wir uns um Themen wie Performance-Gewinne oder die Integration in Web-Frameworks wie Spring, Micronaut oder Quarkus kümmern, zunächst etwas zur Theorie. Wer verstehen will was GraalVM (als Ergänzung oder gar Alternative zur JVM) verspricht zu verbessern, sollte verstehen wie die JVM und GraalVM überhaupt funktionieren. GraalVM ermöglicht die Verwendung verschiedener Sprachen, der Einfachheit halber beschränken wir uns aber in den nächsten Abschnitten auf Java und Kotlin, da wir uns insbesondere auf die Frage fokussieren wollten, ob und inwieweit GraalVM eine Alternative zur JVM sein kann.

Einem/r Entwickler:in von JVM-Sprachen bietet GraalVM zwei verschiedene Modi: JVM Runtime Mode und Native Image Mode. Im Folgenden gehen wir detailliert auf beide Modi ein und beleuchten ihre Vor- und Nachteile.

Schematischer Aufbau des Java Runtime Environments

Fun Fact: Während der HotSpot-Compiler in C++ (und Assembler) geschrieben ist, ist der GraalVM-Compiler selbst in Java geschrieben. Aber was unterscheidet nun den GraalVM- von dem HotSpot-Compiler? Dazu verweist das Team unter anderem auf ein wissenschaftliches Paper. In kurz (und zitiert): […] performance advantages for highly abstracted programs due to its ability to remove costly object allocations in many scenarios. Oder: Better inlining and more aggressive speculative optimizations can lead to additional benefits for complex long-running applications […]. Es fällt auf, dass hier besonders auf abstrakte Sprachen wie etwa Scala abgezielt wird, bei denen Inlinings (auch von Vererbungsstrukturen) und spekulative Optimierungen (welche Programmzweige sollen vorkompiliert werden) vorgenommen werden.

Native Image Mode

Im Native Image Mode wird der JVM-Pfad komplett verlassen. Native Image ist eine optionale Komponente der GraalVM-Software. Anders als üblich wird hier der JVM-Code, also etwa Java und Kotlin, nicht zur Laufzeit kompiliert, sondern schon zur Entwicklungszeit bzw. Build-Zeit (ahead-of-time, AOT). Am Ende des Prozesses kommt eine einzelne, ausführbare Datei heraus, die bereits alles enthält was zur Laufzeit benötigt wird: Anwendungscode, Bibliothekscode und Code, den normalerweise die JRE zur Verfügung stellt. Hierbei wird schon die erste Einschränkung klar: Es muss für jede Zielplattform eigenständig kompiliert werden, was auch bedeutet, dass diese bekannt sein muss.

Aber auch die ausführbare Datei, das Native Image, hat eine VM. Dies ist nicht die JVM sondern die sogenannte SubstrateVM. Diese übernimmt Aufgaben wie die Speicherverwaltung, Garbage Collection und Thread Scheduling.

Damit das Artefakt des Native Images nicht unendlich groß wird, bedient sich die Kompilierung hierbei einer sehr aggressiven, statischen Analyse: Code, der zur Laufzeit ausgeführt werden kann, muss zur Build-Zeit dem Compiler bekannt sein. Das führt zu Problemen wenn man sich etwa Dynamic Class Loading und Reflection bedient. Dieses wird jedoch häufig von modernen Microservice-Frameworks genutzt, sodass Native Image eine Möglichkeit bietet, zur Build-Zeit Informationen bereitzustellen, welcher Code gegebenenfalls in die statische Analyse einbezogen werden muss (mehr dazu in unseren anschließenden Abschnitten über GraalVM im Zusammenspiel mit aktuellen Microservice-Frameworks).

Zahlen bitte

Bei simplen Java-Anwendungen konnten wir kaum einen Performance-Unterschied zwischen dem Standard-JIT-Compiler und dem GraalVM-Compiler finden. Das GraalVM-Projekt zeigt auf ihrer Seite eine Messung bei Nutzung der Stream API mit einer großen Menge an Inputdaten und erzielt damit eine Steigerung um einen Faktor von etwa 4 bis 5.

Bei Native Images sind die Performance-Unterschiede wirklich beträchtlich. Wie sich das auf Speicherverbrauch und Startzeit von Microservice-Frameworks auswirkt, werden wir euch in den nächsten Teilen noch ausführlich zeigen, nur so viel: Wir reden von Startup-Zeiten im zwei- bis dreistelligen Millisekunden-Bereich. Aber auch bei einfachen Java-Programmen sind die Unterschiede signifikant: Die Speicherauslastung beträgt etwa ein Viertel, die Prozessorauslastung etwa ein Zehntel bis ein Sechzigstel.

Prozessor- und Arbeitsspeicherverbrauch des Native Image und JVM Modus unter vergleichbaren Bedingungen

Wichtig zu wissen: All diese Zahlen haben wir mit der Community Edition von GraalVM ermittelt – das ist die Open-Source-Version, lizenziert unter GNU GPL v2. Mit der kostenpflichtigen Enterprise Edition verspricht Oracle in der Regel eine Performanceverbesserung von 20% etwa durch zusätzliche Optimierungsalgorithmen wie Profile-Guided Optimization.

Microservice-Framework-Support

GraalVM wird von allen großen Microservice-Frameworks unterstützt, darunter Quarkus, Spring, Micronaut, Helidon und Gluon. Beachtet jedoch bitte, dass für Native Images die Unterstützung mal größer, mal kleiner ausfallen kann: Bei Quarkus, Spring und Micronaut bekommt ihr Unterstützung bei der GraalVM-Konfiguration durch Annotationen oder Maven-Plugins, die es euch ermöglichen Native Images zu bauen. Bei anderen Frameworks müsst ihr dies manuell machen.

Herausforderungen

Der Umstieg auf GraalVM ist nicht schwer, insbesondere nicht der Umstieg zum GraalVM JVM Mode, da dies nichts an eurem Entwicklungs-, Build- und Deploymentprozess ändert. Vorsicht ist geboten, wenn die Enterprise Edition eingesetzt werden soll: Viele Unternehmen haben sicherlich schon eine Lizenzierung für Java SE. In dieser ist die Enterprise Edition enthalten. Andernfalls muss sie extra lizenziert werden. Aber auch in der Community Edition kann der Umstieg Sinn ergeben: Durch nur wenige Anpassungen im Entwicklungs-, Build- und Deploymentprozess können einige Prozentpunkte Performance gewonnen werden.

Anders sieht es beim Umstieg auf Native Image aus: Hier sind grundlegende Änderungen in den Prozessen notwendig. Im Entwicklungs- und Buildprozess ist der größte Faktor die AOT-Kompilierung. Ihr könnt hier mit zwei- bis zehnfach längerer Buildzeit rechnen und einem Speicherverbrauch, der schnell 10 GB erreichen kann. Das ist eine Herausforderungen in den Aspekten Hardware und Zeit für eine CI/CD-Pipeline. Wenn die CI/CD-Pipeline plötzlich nicht mehr drei oder fünf, sondern zehn oder 15 Minuten braucht ist das ein Hindernis im Entwicklungsprozess. Darüber hinaus sind uns beim Ausprobieren einige Fehler auch erst zur Laufzeit aufgefallen, was den Entwicklungsprozess zusätzlich verlangsamt. Sofern in eurer Architektur ohnehin Docker Images deployt werden ändert sich im Deploymentprozess nichts: Ihr könnt die kompilierte ausführbare Datei statt des WARs/JARs in euer Docker Image packen.

Fazit

GraalVM ist unserer Meinung nach eine spannende Entwicklung. Ob sich die Versprechen der Performancesteigerungen halten können, bleibt abzuwarten und ist ohnehin je nach Anwendungsfall spezifisch und im Einzelnen zu bewerten. Und trotzdem, da der Umstieg zum JVM Runtime Mode so einfach ist, bietet es sich an.

Anders sieht die Sache bei Native Images aus: Hier sind die Eingriffe in Entwicklung, Build und Deployment gewaltig. Native Images sind eine tolle Alternative für Kommandozeilenprogramme bei denen es auf Performance ankommt, etwa bei der Prozessierung von großen Datenmengen. Zusammen mit der riesigen Welt von Java-APIs und -Bibliotheken ergibt dies eine spannende Perspektive. Dies gilt explizit auch für den Bereich von Serverless Computing.

Im Bereich der Enterprise-Architekturen im Zusammenspiel mit (Web-) Frameworks würden wir Native Images jedoch höchstens zum Ausprobieren empfehlen, auch wenn die Anforderungen an provisionierte Hardware bei Native Images deutlich unter denen für klassische JVM Images liegt. In den weiteren Teilen unserer Reihe werdet ihr sehen, dass die Hürden im alltäglichen Einsatz hier noch zu hoch sind.

GraalVM und Native Images ausprobieren

Um ein Gefühl dafür zu bekommen, welche Besonderheiten und Funktionen GraalVM und Native Images bieten, können wir euch diesen Blogartikel ans Herz legen. Er hat auch uns einen guten Einblick in das Thema gegeben.
Wollt ihr GraalVM selbst ausprobieren und installieren, dann hier entlang. Ein Hello-World-Beispiel inklusive Installation von Native Image gibt es auch.