Die JVM
Die Java Virtuelle Maschine gibt es jetzt schon seit 23 Jahren. Ursprünglich wurde sie geschaffen, um eine einfache, objektorientierte, robuste und dynamisch gebundene Programmiersprache plattformunabhängig ablaufen lassen zu können. Die Idee war neu und erfolgreich, schließlich hatte man sich die Jahre davor mit den verschiedensten Compilern auf unterschiedlichen Systemen herumgeschlagen. Dabei war u.a. das Speichermanagement aufwendig und fehlerträchtig. Da hatte die Idee einer JVM etwas revolutionäres, schon alleine weil man sich darum nicht mehr selbst kümmern musste. Mit den Jahrzehnten wuchs die Verbreitung. Unterschiedliche Programmiersprachen entstanden, die für die JVM entwickelt und in deren Bytecode kompiliert wurden. Die Java SDK bekam immer neue Funktionalitäten. Das alles hat die JVM aber auch groß und träge gemacht. Ein Docker Image für die JVM kommt heute nur mit Mühe unter 150 MB und benötigt Sekunden zum starten. Zugute halte muss man ihr zumindest, dass eine kurze Startzeit bisher auch nie erforderlich war. Eine JVM wird eher selten neu gestartet. Die Behandlung beispielsweise neuer Web-Anfragen an sie, erfolgt über neue Threads innerhalb der JVM Instanz und nicht über neu gestartete JVM-Prozesse.
Veränderung durch Container
Veränderungen ergeben sich heute hauptsächlich aus der Container-Technologie. Da wir Container nicht einfach manuell starten, verwenden wir üblicherweise einen Container Manager wie Kubernetes. Er erledigt für uns auch einige andere Anforderungen, wie die Überwachung der korrekten Funktion eines Containers inklusive des Neustarts im Fehlerfall. Ebenso kümmert er sich durch das Erzeugen zusätzlicher Instanzen um deren Skalierung. Abgeschlossene Aufgaben (“Run to completion finite workloads”) wie Berechnungs- oder Backup-Jobs startet bzw. überwacht er ebenfalls und es bedarf keiner eigenen Implementierung.
So wird ein Großteil der Routineaufgaben, die bisher über Frameworks realisiert wurden, auf den Container Manager verlagert. Der Container selbst enthält idealerweise nur noch Code, der aus fachlicher Sicht notwendig ist.
Ein Jar ist ein komprimierter Bytecode, der auf jeder JVM gestartet werden kann. Es dient als Deployment-Artefakt für eine JVM Instanz. Im Gegensatz dazu, beschränkt sich ein Container nicht auf eine spezifische Technologie. Es spielt keine Rolle mehr, welcher Prozess in einem Container gestartet wird. Ob es eine JVM oder das Binärfile einer kompilierten Sprache ist, das Container Image selbst ist das neue, generische Deployment-Artefakt.
Da im Umfeld von Microservice-Architekturen in jedem Fall eine umfangreiche CI/CD notwendig ist, erstellt ein Build-Job nach jedem Commit ein Container Image. Es bereitet keinen großen Aufwand, wenn dieser Job dabei zusätzlich noch Images für unterschiedliche Umgebungen baut, sollte das notwendig sein. “Write Once, Run Everywhere”, einer der ursprünglichen Gründe für den Bytecode, wird so durch ein Container Image viel eleganter und flexibler gelöst.
Aber auch sparsam mit Speicher umzugehen, ist noch immer eine Zierde. Speicher scheint zwar billig, aber in einer hoch skalierbaren Anwendung spielt es bei hunderten von Containern durchaus eine Rolle ob 200 Megabyte oder 20 Megabyte pro Container benötigt werden. Applikationen für die JVM und die JVM selbst geben sich da noch immer recht wenig Mühe.
Wozu also noch den ganzen Ballast einer komplexen JVM und komplexer Frameworks mit herum schleppen? Ein Anzeichen einer zunehmenden Verbreitung dieser Erkenntnis ist die Beliebtheit der Programmiersprache Go. Das Binary enthält nur das, was zum Ausführen des Codes notwendig ist, nicht mehr. Es ist, auch wenn es statisch gebunden ist, sehr klein, benötigt wenig RAM und startet schnell.
So ist es zum Beispiel wieder möglich, pro Request einen Container zu starten. Niemand würde bei einer JVM auf so eine Idee kommen. Die dadurch gewonnene Freiheit aber ist ein Plus an Flexibilität in einer Cluster-Umgebung. Das kann der Container Manager nutzen, denn er hat schließlich einen ganzen Pool von Maschinen zur Verfügung und muss sich nicht auf die Threads innerhalb ein und derselben JVM-Instanz beschränken.
Ist die JVM am Ende?
Nein, ist sie nicht. Die Beliebtheit von Go und die Existenz einiger “Native”-Initiativen (Scala Native, Kotlin Native) allerdings geben einen Hinweis darauf, dass es Zeit ist, sich nicht nur mit der nächsten Sprache für die JVM zu beschäftigen, sondern auch mit anderen Konzepten insgesamt.
Bei durch Container Manager verwalteten, über Prozesse in der Cloud skalierten Containern, machen leichtgewichtigere Ablaufumgebungen besonders viel Sinn. In diese neue Welt scheint eine einfache kompilierte Sprache wie Go mit ihren schlanken Konzepten viel besser zu passen, als die schwergewichtige JVM.