zurück zum Artikel

Was ist neu in Java 7? Teil 3 – Allgemeingültigkeit

Sprachen

Nach den ersten beiden Teilen der Artikelserie zu den Neuerungen von Java 7 geht dieser Beitrag auf die Änderungen der Laufzeitumgebung ein. Es stehen jedoch weniger die Erweiterungen und Änderungen an der Sprache selbst, sondern vermehrt der Einsatz der JVM als Laufzeitumgebung für andere Programmiersprachen im Vordergrund.

Die Java Virtual Machine (JVM) ist der Teil der Java-Laufzeitumgebung (Java Runtime Environment) für Programme, der für die Ausführung des Java-Bytecodes verantwortlich ist. Sie dient als Schnittstelle zur Maschine und zum Betriebssystem und ist für die meisten Systeme (Desktop, Server oder Mobile) verfügbar. Die bekannteste und wohl verbreitetste ist die HotSpot JVM, die auch Teil des Java-7-Downloads ist. Im Hause Oracle gibt es zudem noch das von BEA Systems übernommene JRockit. Es liegt derzeit aber noch nicht in mit Java 7 kompatibler Version vor, und ob Oracle diese überhaupt veröffentlichen wird, ist fraglich, da der Java-Statthalter angekündigt hat, beide Produkte zu vereinen.

Was ist neu in Java 7?

Die Artikel zur Reihe:

Von Beginn an war die JVM nicht darauf beschränkt, nur Java-Applikationen auszuführen. Der von ihr ausführbare Bytecode ist Java-unabhängig und kann von nahezu beliebigen Programmiersprachen stammen,
solange diese einen geeigneten Bytecode-Übersetzer (Compiler) haben. Beim Bytecode handelt es sich um eine maschinenunabhängige Sprache, die die JVM in ausführbaren Maschinencode der jeweiligen Zielplattform umwandelt. Das geschieht "gerade rechtzeitig" ("just in time", JIT [3]). Der finale Übersetzungsvorgang schlägt sich also in der Laufzeit des Programms nieder.

Am Beginn der Programmlaufzeit wird der Bytecode tatsächlich lediglich interpretiert, und erst Schritt für Schritt entscheidet sich die JVM dafür, besonders aufwendige Methoden (Hotspots) in Maschinencode zu übersetzen. Um zu hohe negative Laufzeitaspekte zu vermeiden, wenden die JVMs darüber hinaus unterschiedliche Optimierungsstrategien an, die anhand diverser Parameter den Übersetzungsvorgang und auch anschließende dynamische Optimierungen zur Laufzeit steuern.

Im Laufe der vergangenen zehn Jahre hat das zu einer performanten, plattformunabhängigen, weit verbreiteten und auf Nebenläufigkeit optimierten Laufzeitumgebung geführt. Diese Eigenheiten der JVM haben auch bei Entwicklern anderer Programmiersprachen Begehrlichkeiten geweckt. Vor allem Ruby und Python haben früh den Sprung auf die JVM gewagt, und es entstanden die Implementierungen JRuby und Jython.

Größtes Manko war aber, dass die Vielzahl der Optimierungen der JVM auf die Sprache Java abgestimmt war. Unterschieden sich die anderen Programmiersprachen deutlich, beispielsweise in der Art, wie Mechanismen zur Auflösung von Objekten oder Methoden funktionieren, war die JVM nicht immer die optimale Ausführungsumgebung. Besonders komplex wurde es, wenn es sich um unterschiedliche Typsysteme handelte, denn Java ist eine Sprache, die statische Typisierung zur Übersetzungszeit verwendet. Deshalb war die JVM für die Ausführung statisch typisierter Sprachen optimiert.

Bei statischer Typisierung wird im Gegensatz zu dynamischer der Datentyp von Variablen und anderen Programmbausteinen während der Kompilierung festgelegt. Ruby und Python sind (zumindest teilweise) dynamisch typisiert, und der Programmcode enthält vielfach keine Informationen über Datentypen zur Übersetzungszeit. Sie lassen sich nur zur Laufzeit bestimmen. Diese Eigenart der JVM war bis dato sicherlich gewollt und hat auch zur Verbreitung und zum allgemeinen Erfolg der Programmiersprache Java beigetragen. Das hat aber die Compiler-Entwicklung für dynamisch typisierte Sprachen deutlich erschwert.

Die Herausforderung

Ein einfaches Beispiel, geschrieben in einer fiktiven Programmiersprache, soll die Schwierigkeit bei der Übersetzung dynamisch typisierter Sprachen verdeutlichen:

def addiereZwei(a, b)
a+ b
end

Das Verhalten des Additions-Operators "+" unterscheidet sich je nach eingesetzter Typisierung. Bei einer statisch typisierten Sprache entscheiden die Datentypen a und b über die tatsächliche Implementierung. Der Java-Compiler würde ein "+" auf zwei Integer-Typen mit der JVM-Instruktion "iadd" übersetzen:

aload_0       
invokevirtual #6 // Method java/lang/Integer.intValue:()I
aload_1
invokevirtual #6 // Method java/lang/Integer.intValue:()I
iadd

Im Gegensatz dazu muss ein Übersetzer für eine dynamische Sprache die Auflösung der Operator-Methode bis zur Laufzeit verzögern. Ein entsprechender Aufruf müsste lauten:

call +(a,b)

Dabei muss die Laufzeitumgebung herausfinden können, um welche Typen es sich bei den Variablen a und b handelt und eine für diese Typen geeignete Implementierung der Additions-Methode "+" verwenden. Einfach alle Variablen so behandeln, als wären es Objekte, funktioniert nicht, da java.lang.Object keine "+"-Operation kennt.

JSR 292

Antwort durch Java 7

Neu sind die beschriebenen Herausforderungen nicht. Mit dem JSR 223 [4] (Scripting for the Java Platform) wurde schon in Java 6 versucht, Script-Sprachen an Java anzubinden, indem Script-Engines den Code der gewünschten Sprache zur Laufzeit ausführten. Neben Mozillas Rhino als Referenzimplementierung für JavaScript ließen sich Ruby, PHP und Groovy einsetzen. Dieser Ansatz half zwar bei der Anbindung unterschiedlicher Sprachen, die Probleme beim Compiler-Bau blieben jedoch unverändert.

Die Verwendung der JVM als Laufzeitumgebung für andere Programmiersprachen geht mit Java 7 der JSR 292 [5] (Supporting Dynamically Typed Languages on the Java Platform) an. Java 7 hat zum ersten Mal in der Geschichte von Java neuen Bytecode mit an Bord: Mit der neuen Anweisung InvokeDynamic lassen sich Methoden ohne vorherige Prüfung aufrufen, zu welcher Klasse die Methode gehört, von welchem Typ ihr Rückgabewert ist oder welche Methodenparameter sie verwendet. Mit ihrer Hilfe lässt sich die Verbindung zwischen dem Aufrufer und der Methoden-Implementierung anpassen. Ausgangspunkt ist dabei die Aufrufer-Seite (call site). Dabei wird eine "InvokeDynamic call site" durch eine Bootstrap-Methode an eine Methode gebunden. Das erfolgt einmalig. Muss die JVM jetzt eine InvokeDynamic-Instruktion (bspw. "+") ausführen und kennt sie eine entsprechende Implementierung addiereZwei(Integer, Integer) in einer Klasse, lässt sich diese verlinken:

public static CallSite bootstrap(
MethodHandles.Lookup callerClass, String dynMethodName,
MethodType dynMethodType)
throws Throwable {

MethodHandle mh =
callerClass.findStatic(
Beispiel.class,
"MeineTypen.addiereZwei",
MethodType.methodType(Integer.class, Integer.class, Integer.class));

if (!dynMethodType.equals(mh.type())) {
mh = mh.asType(dynMethodType);
}

return new ConstantCallSite(mh);
}

Dabei stellt die Klasse MeineTypen einen Teil des Typsystems der dynamischen Sprache dar. Die abgebildete Beispielklasse übernimmt das Bootstrapping der Verbindung. Beachtenswert ist es, dass in dem Beispiel alle Aufrufe und die Methode "addiereZwei" davon ausgehen, dass es sich um Argumente vom Typ "Integer" handelt. In einer konkreten Implementierung wäre hierbei mit Rückfallmöglichkeiten die entsprechende Varianz der Typen zu berücksichtigen. Im Falle von JRuby sieht der generierte Bytecode mit der Anweisung
InvokeDynamic folgendermaßen aus.

aload_1       
aload_2
aload 10
ldc #67 // String +
aload 11
invokedynamic #76, 0 // InvokeDynamic
// #1:call:( (...)[invocationBootstrap(...)]
areturn

Auch wenn das Codebeispiel den Eindruck erweckt, dass die neue Funktion dem Java-Entwickler zur Verfügung steht, ist dem tatsächlich nicht so. Es handelt sich zwar um Klassen des JDK, diese sind allerdings lediglich für einen Implementierer von JVM-Sprachen hilfreich. Am nächsten kommt man dem neuen Bytecode noch beim Einsatz von Bytecode-Manipulation oder -Analyse. Das ASM [6]-Framework ist hierfür sicherlich ein bekannteres Beispiel. Da die neue Bytecode-Anweisung für Java in Summe nicht notwendig ist, kommt sie auch nicht im Java-Compiler zum Einsatz.

Für den Hauptentwickler von JRuby, Charles Oliver Nutter, stellt gerade diese Neuerung allerdings eine echte Revolution dar [7]. Durch den starken Gebrauch von InvokeDynamic ließen sich offenbar Performancegewinne von bis zu 200 Prozent erzielen. Details über die Verwendung von InvokeDynamic bei JRuby finden sich auch in Nutters Slides eines auf dem JVM Language Summit gehaltenen Vortrags (PDF [8]). Für Java 8 ist das als Grundlagenarbeit zu verstehen. Die für die kommende Version geplanten Closures für Java (Project Lambda) werden nach aktueller Planung ebenfalls mit der InvokeDynamic-Anweisung arbeiten, wie Brian Goetz, Oracles Architekt für die Java-Sprache, auf der gleichen Veranstaltung aufgezeigt hat (PDF [9]).

Classloading

Classloader-Architektur

Ähnlich tief in der Laufzeit vergraben versteckt sich die nächste Änderung, die JRE 7 mitbringt. Bereits seit dem JDK 1.3 wird eine Änderung des Classloading-Verhaltens gewünscht [10]. Classloader müssen dem Delegationsprinzip folgen und zuerst ihren Vater-Classloader aufrufen. Wird via eigenem Classloader eingegriffen, kann es bei dem Vorgehen zu einem Deadlock kommen, da die relevante Klasse java.lang.ClassLoader.loadClassInternal(String name) "private synchronized" ist und sich somit weder geeignet überschreiben noch mit einem eigenen Synchronisationsmechanismus versehen lässt. Es ist eine Änderung im Classloader durchzuführen, die das parallele Laden mehrerer Klassen ohne Deadlock ermöglicht. Dazu hat Oracle neben dem internen Sperrmechanismus des Classloader auch die Granularität der Sperren angepasst. In der API sind damit zwei neue Methoden entstanden:

java.lang.ClassLoader.registerAsParallelCapable()
java.lang.ClassLoader.getClassLoadingLock(String className)

Classloader, die das gleichzeitige Laden von Klassen ermöglichen und nicht strikt hierarchisch sind, heißen jetzt "parallel capable" und müssen sich während der Initialisierung mit registerAsParallelCapable() anmelden. Das ist auch der Standardfall für alle seine Oberklassen mit Ausnahme von java.lang.Object. Die Registrierung lässt sich nachträglich nicht rückgängig machen.

Von ähnlicher Qualität sind auch die Änderungen am URLClassLoader. Diese Klasse ermöglicht das Erstellen der Classloader von definierten Ressourcen (Verzeichnissen, Dateien etc.). Sollten diese in der Vergangenheit angepasst werden, waren alle Referenzen auf das Objekt zu löschen, und man musste auf den Garbage Collector (GC) warten. Vorher ließen sich die Ressourcen nicht löschen. Da das GC-Verhalten von Java nicht vorhersagbar ist, entstanden zunehmend Probleme. Vornehmlich unter Windows werden Ressourcen nämlich erst gelöscht, wenn keine Referenzen mehr von einem Programm gehalten werden. Für den Fall steht jetzt die close()-Methode zur Verfügung. Sie invalidiert den Classloader und alle daran gebundenen Ressourcen.

        URL url = new URL("file:heise.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class cl = Class.forName("Developer", true, loader);
Runnable developer = (Runnable) cl.newInstance();
developer.run();
loader.close();

Fazit

Die in diesem Artikel beschriebenen Funktionen stellen sicherlich tiefgreifende Änderungen an der Java-Spezifikation dar. Seit Java 1.0 wurde erstmalig neuer Bytecode eingeführt, und auch die Erweiterungen am Classloader sind vergleichbar wichtige Änderungen. Dennoch wurde anscheinend eine vollständige Abwärtskompatibilität bewahrt. Für Java ein Qualitätsmerkmal, das auch in großem Umfang zum Erfolg der vergangenen Jahre beigetragen hat. Leider bleibt für die tägliche Arbeit der Entwickler nicht viel übrig von diesen Anpassungen.

Die für InvokeDynamic hinzugekommene API lässt sich in Java nur bedingt einsetzen und steht dem Entwickler nur indirekt durch weitere Frameworks (etwa ASM) zur Verfügung. Die Veränderungen am Classloader-Verhalten dürfen auch nur einen geringen Prozentsatz an Entwicklern betreffen. Dennoch sind gerade die hier vorgestellten Weiterentwicklungen die Grundlage für die Weiterentwicklung von Java. (ane [11])

Markus Eisele
ist Principle IT Architect bei der msg systems AG in München.


URL dieses Artikels:
http://www.heise.de/-1340995

Links in diesem Artikel:
[1] https://www.heise.de/developer/artikel/Was-ist-neu-in-Java-7-Teil-1-Produktivitaet-1274360.html
[2] https://www.heise.de/developer/artikel/Was-ist-neu-in-Java-7-Teil-2-Performance-1288272.html
[3] http://de.wikipedia.org/wiki/Just-in-time-Kompilierung
[4] http://www.jcp.org/en/jsr/detail?id=223
[5] http://www.jcp.org/en/jsr/summary?id=292
[6] http://asm.ow2.org/
[7] http://blog.headius.com/2011/08/jruby-and-java-7-what-to-expect.html
[8] http://www.wiki.jvmlangsummit.com/images/3/3e/2011_Nutter.pdf
[9] http://www.wiki.jvmlangsummit.com/images/1/1e/2011_Goetz_Lambda.pdf
[10] http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4406709
[11] mailto:ane@heise.de