© Dogora Sun/shutterstock.com
Neue polyglotte Programmierung auf der JVM

Das eierlegende Truffle-Schwein


Viele, die im Java-Umfeld unterwegs sind, werden von ihr gehört haben: der sagenumwobenen GraalVM. Diese „magische“ neue Virtual Machine für Java soll vor allem für blanke Performance sorgen, indem sie den Java-Bytecode in nativen Code kompiliert. Dadurch fällt insbesondere der Start-up-Overhead weg, da weite Teile der Initialisierung bereits vom Compiler erledigt werden. So oder so ähnlich ist es vielerorts zu lesen [1], [2]. Doch das ist bei weitem nicht das einzige Feature, das Oracle der GraalVM gegeben hat. Hinzu kommt, dass sie zu nichts weniger das Potenzial hat, als eine neue Ära der polyglotten Programmierung auf der JVM einzuläuten. Die Rede ist vom Truffle API, einem generischen Framework zur Implementierung von Interpretern.

Als die JVM 1994 neu herauskam, war sie noch untrennbar mit Java, der Sprache, verbunden. Deutlich zu erkennen ist, dass der JVM-Bytecode so angelegt worden war, dass sich Javas „Geschmacksrichtung“ der objektorientieren Programmierung gut darin abbilden ließ. Das ist aus historischer Sicht konsequent, denn schließlich sollte die JVM den kompilierten Java-Code effizient ausführen können.

Über die Jahre hinweg bekamen sowohl die JVM als auch Java neue Features, Optimierungen und Verbesserungen. Klar war, dass Sun Rückwärtskompatibilität als höchstes Gut auffasste. So sollten einmal kompilierte Java-Programme praktisch unbegrenzt in zukünftigen JVM-Versionen lauffähig bleiben.

Die Krönung der Rückwärtskompatibilität erfolgte dann knapp zehn Jahre später mit der Veröffentlichung von Java 5. Im Java-Compiler wurde das Typsystem durch die Einführung von Generics kräftig umgekrempelt. Doch am Bytecode änderte sich recht wenig: Generische Typen werden vom Compiler kurzerhand entfernt, um Kompatibilität mit existierendem Code nicht zu gefährden. Man spricht von Type Erasure.

Anders getypte und ungetypte Sprachen

Parallel zu diesen Entwicklungen haben kluge Köpfe an alternativen Sprachdesigns gearbeitet, die mehr oder weniger an Java angelehnt sind.

Bei Scala, das ungefähr zeitgleich mit Java 5 erschien, hat sein Erfinder Martin Odersky das Grundkonzept der Objektorientierung beibehalten, alte Zöpfe abgeschnitten und gleichzeitig funktionale Programmierung auf der JVM salonfähig gemacht. Dank Type Erasure konnte Odersky sich im Typsystem austoben und musste nur wenig Rücksicht auf die Generics in Java 5 nehmen. Scalas fortgeschrittenes Typsystem spielt so in dem generierten Bytecode gar keine Rolle mehr.

Ungefähr ein Jahr zuvor war bereits die Sprache Groovy erschienen, die ursprünglich als standardisierte Skriptsprache auf der JVM antreten wollte. Aus dem zugehörigen JSR 241 wurde jedoch nichts, und Groovy entwickelte sich zu einer unabhängigen Programmiersprache. Im Gegensatz zu Java verzichtet Groovy an vielen Stellen auf Typen, sodass Methoden häufiger per Reflection aufgerufen werden als in Java. Erst zur Laufzeit wird klar, in welcher Klasse die jeweilige Methode implementiert ist. Für einen Aufruf ohne Reflection müsste die konkrete Methode aber schon feststehen, wenn der Bytecode generiert wird. Also muss man den Reflection Overhead hinnehmen.

Ist die JVM groß genug für mehrere Sprachen?

Scala und Groovy sind bei weitem nicht die einzigen Beispiele für alternative JVM-Sprachen. Manche sind typisiert, andere eher dynamisch. Allen ist jedoch gemeinsam, dass die JVM wegen ihrer Performance und bestehender Bibliotheken eine für sie attraktive Plattform darstellt. Auch Portierungen existierender Sprachen (Jython, JRuby) wurden in den 2000er-Jahren zunehmend populär.

2011 folgte dann Java 7, das die lang erhoffte Bytecodeerweiterung invokedynamic brachte. Damit lassen sich sehr effizient dynamische Methodenaufrufe implementieren, die in ungetypten Sprachen dominieren. Vorangegangen war die Arbeit an der sogenannten Da Vinci Virtual Machine [3], mit der Sun an einem First-Class-Support für andere Sprachen auf der JVM experimentiert hat.

In Java selbst werden nicht statische Methodenaufrufe mit den Instruktionen invokevirtual oder invokeinterface durchgeführt. Dabei sind die Argumenttypen bereits festgelegt, aber die implementierende Klasse wird zur Laufzeit ausgewählt. Im einfachsten Beispiel:

class A { int f() { /* ... */ } } class B extends A { @Override int f() { /* ... */ }}

Je nachdem, welcher Klasse ein Objekt x angehört, wird beim Aufruf x.f() entweder A.f() oder B.f() ausgeführt.

In ungetypten Sprachen hingegen müssen Methoden regelmäßig auch nach ihren Argumenten ausgewählt werden, um z. B. zwischen der Addition von Zahlen oder von Strings zu unterscheiden. Dazu dient invokedynamic, wozu die Oracle-Dokumentation [4] Folgendes erläutert: „[It] enables the runtime system to customize the linkage between a call site and a method implementation.“

Implementierungsaufwand

Ambitionierten Entwickler*innen, die ihrer Sprache ein JVM Backend verpassen wollen, steht eigentlich nichts im Wege. Wäre da nicht das Problem des Compilerbaus. Oftmals werden daher Interpreter statt Compiler für Sprachen gebaut, denn dann kann man die Laufzeitinfrastruktur der Hostumgebung einfach direkt benutzen.

Im Gegensatz dazu ist es sehr schwierig, maßgeschneiderten, korrekten JVM-Bytecode zu produzieren. Die invokedynamic-Instruktion ist ein Paradebeispiel hierfür: Jeder derartige Aufruf erfordert eine Bootstrap-Methode, die den korrekten Method Handle – ebenfalls eine Neuerung in Java 7 – bereitstellt. Diese Handles lassen sich zwar viel effizienter aufrufen als die üblichen Reflection Methods, haben aber ein komplexeres API. Eine Einführung in dieses API gibt es z. B. unter [5].

Doch nur mit einem „echten“ Compiler kann man effektive Programmoptimierungen vornehmen. Ein Interpreter auf Basis der JVM hat zudem immer das Problem, dass man zwei Interpreterschichten aufeinander hat (JVM-Bytecode durch die JVM, Sprachquelltext durch den Sprachinterpreter). Dass das der Performance nicht sonderlich zuträglich ist, überrascht nicht.

Futamuras Vision

Wie wäre es also, wenn es ein System gäbe, das einen Sprachinterpreter vollautomatisch in einen Sprachcompiler tra...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang