1.2 JVM-Änderungen
Wenn es um Java geht, müssen wir immer unterscheiden, ob es um die Sprache selbst geht, um die Bibliotheken, um die JVM oder um die Implementierung von Oracle, die das JDK darstellt. Eine wichtige Änderung am Bytecode, und damit an der JVM selbst, ist ein neuer Bytecode zum Beschleunigen von Methoden, deren Signatur nicht bekannt ist.
In der Implementierung von Oracle gibt es zwei große Änderungen an der JVM selbst:
- einen neuen Garbage-Collector G1 (für Garbage Frist)
- komprimierte 64-Bit-Objekt-Pointer, sodass sie in 32 Bit Platz finden.
Bei Java-Programmen mit DTrace (wie Solaris) lassen sich die Zustände erfragen und die Abarbeitung überwachen.
1.2.1 invokedynamic
Seit Java vor über 10 Jahren auf dem Markt erschien, hat sich vieles geändert (und auch vieles ist gleich geblieben). Auffällig ist eine starke Zunahme von Skriptsprachen – spielten sie vor 10 Jahren kaum eine Rolle, sind sie heute unübersehbar. Möglicherweise ist ein Grund dafür, dass vor 10–20 Jahren compilierte Sprache einfach nicht die nötige Performance brachten, aber heute auch durch leistungsfähige Maschinen und intelligente Ausführung die Leistung einfach da ist.
Skriptsprachen auf der JVM
Eine unübersehbare Tatsache ist, dass viele Skriptsprachen heute in einer virtuellen Maschine laufen. Besondere Rolle nehmen die Java-Plattform (mit der JVM) und die .NET-Plattform mit CLR (einer virtuellen Maschine für .NET) ein. Zwei Trends zeigen sich: Zum einen werden existierende Skriptsprachen auf die JVM/CLR übertragen und zum anderen Sprachen explizit für die virtuellen Maschinen entworfen.
Skriptsprache | Für JVM | Für CLR |
Python |
Jython |
IronPython |
Ruby |
JRuby |
IronRuby |
Lua |
Jill, Kahlua |
LuaCLR |
JavaScript |
Rhino |
IronJS |
PHP |
Quercus |
|
Tcl |
Jacl |
|
|
Groovy |
|
|
JavaFX |
|
|
|
Boo |
Umsetzung der Skriptsprachen auf der JVM
Eine Skriptsprache, die explizit für eine VM entworfen wurde, berücksichtigt natürlich Einschränkungen. Existierende Programmiersprachen sind eine ganz andere Herausforderung, da sie Sprach- und Laufzeitkonstrukte bieten können, die auf der JVM vielleicht nicht unterstützt werden. Das ist ein Problem, und drei Strategien zur Lösung bieten sich an:
- Ignorieren der Eigenschaften: Python ermöglicht Mehrfachvererbung, Java nicht. Daher kann das Verhalten von Python unter Java nicht perfekt nachgebildet werden, obwohl Jython sein Bestes gibt. Weiterhin kann Python Interrupts auffangen, aber so etwas gibt es unter Java nicht, also auch nicht in Jython. (Das ist aber eher eine Bibliotheks- und weniger eine Spracheigenschaft.)
- Nachbilden der Eigenschaften: In Sprachen wie JavaScript, Python oder Ruby gibt es das sogenannte Duck Typing. Zur Laufzeit wird erst das Vorhandensein von Methoden geprüft, und die Typen müssen nicht im Quellcode stehen. In JavaScript ist zum Beispiel function add(x, y){return x + y;} oder function isEmpty(s){return s == null || s.length() == 0;} erlaubt, und erst später beim Aufruf stellt sich heraus, was x und y überhaupt ist und ob ein Plus-Operator/length()-Methode definiert ist. Deklarationen und Aufrufe dieser Art sind nur mit vielen Tricks auf der JVM umzusetzen.
- Ändern der JVM zur Unterstützung der Eigenschaften: Die Spracheigenschaften zu ignorieren ist natürlich keine schöne Sache. Glücklicherweise kommt das selten vor, denn die JVM macht vieles mit. Ruby erlaubt etwa Methodennamen wie ==, <, >, +, und 1.+2 ist gültig. Die Bezeichner sind in Java zwar nicht möglich, aber die JVM hat grundsätzlich mit den Bezeichnen keine Probleme. Es ist interessant zu sehen, wie unterschiedlich Java als Sprache und die JVM sind, denn vieles in Java gibt es in der JVM gar nicht. Enums zum Beispiel sind nur einfache Klassen. Oder dass eine Klasse ohne einen vom Entwickler explizit geschriebenen Konstruktor automatisch einen Standardkonstruktor bekommt, ist nur etwas, was der Compiler generiert. Oder ein super() als ersten Aufruf im Konstruktor – alles das sind Compilereigenschaften, und von Generics ganz zu schweigen.
Wenn die Besonderheiten einer Skriptsprache über Tricks und intelligentes Übersetzen in Bytecode realisiert werden können, stellt sich die Frage, ob an der JVM überhaupt Änderungen nötig sind. Wie bei vielen Diskussionen kommt dann ein Argument auf, das Änderungen oft rechtfertigt: Performance. Wenn eine Änderung in der JVM die Abarbeitung bestimmter Konstrukte massiv beschleunigt, ist das ein Grund für die Änderung. Und somit sind wir beim neuen Bytecode invokedynamic angelangt.
Umsetzung von Duck Typing
Reine Java-Programme werden den Bytecode invokedynamic nie benötigen, er ist einzig und allein in die JVM eingeführt worden, um Methodenaufrufe der Skriptsprachen auf der JVM zu beschleunigen. Die bisherigen Bytecodes von Methodenaufrufen trugen alle nötigen Typinformationen, so wie es bei Java üblich ist. Nehmen wir:
System.out.prinln( 1 );
Der Compiler macht aus dem Programm ein invokevirtual, wobei genau bei dem Aufruf steht, dass ein println() gemeint ist, was ein int annimmt. Die Typinformationen im Bytecode sind:
- Der Empfänger ist PrintStream, und seine Methode ist println.
- Der Parametertyp ist ein int.
- Die »Rückgabe« ist void.
Exakt diese Typen müssen vom Compiler in Bytecode gegossen werden, sonst kann die JVM den Aufruf nicht durchführen.
Skriptsprachen hingegen sind da lascher. Nehmen wir noch einmal unser Beispiel aus JavaScript:
function isEmpty(s) { return s == null || s.length() == 0; }
Die Typen wären zwar zur Laufzeit bekannt, aber eben nicht zur Compilezeit, in der der Bytecode erstellt würde. Ein Aufruf der Methode wird schwierig, denn es fehlen ganz offensichtlich der Rückgabe- und der Parametertyp. Natürlich könnten sie über einen Trick zu Object ergänzt werden. Spielen wir kurz die Konsequenzen durch:
Object isEmpty( Object s )
Das sieht gut aus. Aber jetzt wird es haarig bei length(), das keine Methode von Object ist. Das Problem kann ein Compiler auf zwei Arten lösen. Zunächst kann der Aufruf an das unbekannte Objekt, das ja zur Laufzeit üblicherweise etwas anderes als String ist, über Reflection gelöst werden. Da Reflection-Aufrufe aber nicht so performant sind wie richtige Methodenaufrufe, ist diese Lösung nicht so effizient. Eine zweite Lösung besteht in der Einführung einer Schnittstelle, die genau die fehlenden Operationen deklariert:
interface I { int length(); }
I isEmpty( I s ) { return s == null || s.length() == 0; }
Ja, das geht – irgendwie. Das ist performanter als Reflection, aber auf diese Weise kommen eine riesige Anzahl neuer Schnittstellen bzw. Klassen in die JVM, deren Behandlung recht speicherintensiv ist. Außerdem können die Deklarationen von Methoden in Skriptsprachen auch außerhalb von Klassen vorkommen, sodass für Java ein Fake-Methoden-Empfänger erzeugt werden müsste.
invokedynamic und Hilfsklassen
Um die sonderbare Methodendeklaration in Java performant umzusetzen, hat der JSR-292, »Supporting Dynamically Typed Languages on the Java Platform«, den neuen Bytecode invokedynamic zusammen mit einigen Hilfsklassen im Paket java.lang.invoke definiert. Der neue Bytecode muss von einer Java 7-JVM unterstützt werden und wird von dynamischen Skriptsprachen generiert, um die Aufrufe schnell von der JVM ausführen zu lassen. Normale Entwickler werden den neuen Bytecode kaum brauchen, und im Quellcode ist er eh nicht sichtbar.
Weitere Details und ein Programmbeispiel finden Leser unter http://java.sun.com/developer/technicalArticles/DynTypeLang/. Sehr tiefe Detailinformationen liefert http://blogs.oracle.com/jrose/resource/pres/200910-VMIL.pdf.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.