Was ist InvokeDynamic und warum ist es wichtig?

Bytecode 2.0

Markus Heiden

Für viele Entwickler fast unbemerkt hat mit Version 7 eine der größten Neuerungen seit Langem Einzug in Java gehalten: InvokeDynamic. Aber warum kommt man damit als Java-Entwickler so wenig in Berührung? In diesem Artikel erkläre ich, was InvokeDynamic eigentlich ist und wie es eingesetzt werden kann.

Mit Java 7 wurde der JSR-292 umgesetzt, der zum Ziel hat, die Unterstützung dynamischer Sprachen auf der Java Virtual Machine (JVM) zu verbessern, genauer gesagt, auf eine Stufe mit der Unterstützung von Java selbst zu stellen. Dynamische Sprachen gab es bereits vor Java 7 auf der JVM und sie waren auch schon recht performant. Wo also lag das Problem?

Die JVM an sich ist mit ihren ca. 200 Befehlen (Bytecodes) ziemlich sprachneutral definiert und eignet sich damit grundsätzlich auch für dynamische Sprachen. Lediglich die Bytecodes zum Aufruf von Methoden stellen die Entwickler dynamischer Sprachen vor Herausforderungen, da zur Compile-Zeit nicht nur die Signatur der Methode genau bekannt sein muss, sondern die aufzurufende Methode auch bereits konkret an der entsprechenden Klasse existieren muss. Da dies bei dynamischen Sprachen oft nicht gegeben ist, führt das dazu, dass für jeden Methodenaufruf viel eigentlich unnötiger Code generiert werden muss, um die Semantik der dynamischen Sprache auf die Methodenaufruf-Semantik der JVM abzubilden.

Zur Lösung des Problems wurde etwas gemacht, was es bis jetzt in Java nicht gegeben hat. Es wurde ein neuer Bytecode definiert: InvokeDynamic. InvokeDynamic ist also ein Methodenaufruf-Bytecode, der die erwähnten Einschränkungen nicht aufweist.

Um InvokeDynamic zu verstehen, ist ein genaues Verständnis von Methodenaufrufen nötig. Ich erkläre dazu, was die JVM bei einem Methodenaufruf macht, um zu zeigen, wie Methodenaufrufe effizient ausgeführt werden können. Außerdem zeige ich, wie das API im Package java.lang.invoke einen Methodenaufruf abbildet und dabei die Optimierungsmöglichkeiten der JVM zur Verfügung stellt.

Methodenaufrufe

Methodenaufrufe bestehen aus zwei Teilen. Der erste Teil ist ein Methodenaufruf-Bytecode, der die Stelle definiert, von der aus eine Methode aufgerufen wird, die CallSite. Das Ziel wird mit der Signatur der Zielmethode beschrieben, dem MethodHandle. Die eigentliche „Magie“ passiert, wenn die JVM den Methodenaufruf ausführt und dazu die CallSite mit dem MethodHandle verbindet, das so genannte Linken.

In Java ist das Linken relativ einfach, da zur Compile-Zeit bereits größtenteils klar ist, welche konkrete Methode von einem Methodenaufruf aufgerufen wird. Lediglich bei Vererbung muss zur Laufzeit noch etwas Arbeit geleistet werden, wenn überschriebene Methoden aufgerufen werden sollen. Dazu führen Klassen eine Methodentabelle mit sich, in der jede virtuelle Methode einen eigenen Index zugewiesen bekommt. Wenn eine Klasse eine Methode überschreibt, schreibt sie ihre neue Methode an den Index der überschriebenen Methode. Das heißt zur Laufzeit muss nur ein indizierter Zugriff auf diese Tabelle erfolgen, um die konkrete Implementierung der Methode zu bestimmen, die aufgerufen werden soll. Dieses Vorgehen ist bereits relativ schnell, trotzdem kann die JVM diese Art von Methodenaufrufen noch optimieren. Aber dazu später mehr.

Bei dynamischen Sprachen ist die Sache deutlich komplizierter. Zum Einen muss viel öfter zur Laufzeit bestimmt werden, welche konkrete Methode aufgerufen werden soll. Zum Anderen ist das Bestimmen der konkret aufzurufenden Methode nicht so einfach wie bei Java, da es hier oft prinzipbedingt keine Methodentabelle geben kann, die diese Arbeit beschleunigt. Der Nachteil dynamischer Sprachen ist also die Flexibilität ihres Typsystems, die dazu führt, dass bei Methodenaufrufen zur Laufzeit aufwändig bestimmt werden muss, welche konkrete Methode ausgeführt werden soll. Dieser Nachteil soll mit InvokeDynamic vermieden werden.

JVM und Optimierungen

Die JVM arbeitet bei der Ausführung von Bytecode in zwei verschiedenen Phasen. In der ersten Phase führt die Hotspot JVM den Bytecode im Interpreter aus und protokolliert zum Beispiel bei einem Methodenaufruf, welchen dynamischen Typ das Objekt hat, an dem die Methode aufgerufen wird. Nachdem sie das für eine Weile getan hat, wird in der zweiten Phase der Bytecode vom Just-In-Time-Compiler (JIT) in nativen Maschinencode übersetzt. Dabei macht die JVM von den protokollierten Informationen Gebrauch, um Optimierungen vornehmen zu können.

Wenn die JVM beispielsweise feststellt, dass bei einem Methodenaufruf während der ersten Phase immer nur eine bestimmte Implementierung einer virtuellen Methode aufgerufen wird, dann trifft sie die Annahme, dass das wohl auch in Zukunft so sein wird. Deswegen kann sie den indizierten Zugriff auf die Methodentabelle vermeiden und direkt die konkrete Methode aufrufen.

Was passiert, wenn später doch die Methode an Objekten einer anderen Subklasse aufgerufen wird? Um in solchen Situationen den Bytecode nicht fehlerhaft auszuführen, generiert der JIT direkt vor dem optimierten Methodenaufruf nativen Code, der prüft, ob die Annahme noch stimmt, einen Guard. Wenn die Annahme nicht mehr stimmt, löst der Guard ein Neukompilieren des Methodenaufrufs aus, der die Optimierung entweder ganz entfernt oder durch eine weniger aggressive Optimierung ersetzt.

Die Übersetzung der Bytecodes in nativen Maschinencode und diese Art der Optimierung sind die Gründe, warum Java heutzutage die Performance von nativem Code erreichen kann. Gute Performance ist ein Hauptkriterium für die Akzeptanz einer Sprache und deswegen ist es für JSR-292 wichtig, dass diese Performance der JVM auch für dynamische Sprachen genutzt werden kann. Das wird dadurch erreicht, dass diese Art der Optimierung durch InvokeDynamic nicht mehr nur der JVM selber zur Verfügung steht, sondern auch direkt genutzt werden kann. Das Schöne dabei ist, dass diese Möglichkeit nicht nur für dynamische Sprachen genutzt werden kann, sondern auch für Java selber.

Jetzt kommt der konkrete Teil: das Package java.lang.invoke. Als erstes stelle ich die MethodHandles vor, den Kern der neuen Methodenaufrufe. Danach folgen die CallSites, die zusammen mit InvokeDynamic die Dynamik der Methodenaufrufe bereitstellen.

MethodHandles

MethodHandles sind in Java als Klasse MethodHandle direkt sichtbar. Sie lassen sich wie Reflection verwenden, sind aber schneller. Um an ein MethodHandle zu kommen, muss es per MethodHandles.Lookup erzeugt werden. Wichtig dabei ist, welches Lookup-Objekt verwendet wird, da der Sichtbarkeitsbereich des Aufrufers(!) von MethodHandles.lookup() entscheidet, welche Methoden zu sehen sind. Dadurch wird Missbrauch vermieden, da zum Beispiel MethodHandles zu privaten Methoden nur von der Klasse selber erzeugt werden können, solange sie ihr Lookup nicht nach außen zur Verfügung stellt. Außerdem ermöglicht dieses Vorgehen, dass die Prüfung, ob ein Zugriff auf die Methode erlaubt ist, nur einmalig beim Erzeugen des MethodHandles durch das Lookup erfolgt, statt wie bei Reflection bei jedem Zugriff. Das ist ein Grund für die bessere Performance von MethodHandles. Wenn der JIT die Zugriffe erst mal optimiert hat, sind Reflection und MethodHandles aber in etwa wieder gleich schnell.

Das erstes Beispiel (Klasse HelloWorld) ist ein einfaches „Hallo Welt!“ mit MethodHandles. Es wird einfach System.out.println(„Hallo Welt!“) aufgerufen:

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(void.class, String.class);
MethodHandle println = lookup.findVirtual(PrintStream.class, "println", type);
println.invoke(System.out, "Hallo Welt!");

Als Erstes wird ein Lookup geholt. Da PrintStream.println(String) public ist, spielt die Sichtbarkeit keine Rolle. Es wird eine virtuelle Methode gesucht und deswegen findVirtual() verwendet. Zum Finden der Methode werden deren Klasse (PrintStream), ihr Name (println), ihr Rückgabetyp (void) und ihre Parametertypen (String) benötigt. Die für InvokeDynamic relevante Signatur (Rückgabetyp und Parametertypen) wird dabei von MethodType gekapselt. Mit dem MethodHandle kann schließlich die Methode einfach per invoke(…) aufgerufen werden. Dazu werden die Instanz, an der die Methode aufgerufen werden soll, und die Methodenparameter übergeben.

Bei der Suche nach public-Methoden kann man auch MethodHandles.publicLookup() statt MethodHandles.lookup() verwenden, was etwas effizienter ist, da nicht jedes Mal ein neues Lookup-Objekt erzeugt werden muss. Neben findVirtual() gibt es unter anderem noch findStatic() für statische Methoden und findConstructor() für Konstruktoren. Neben Methoden bekommt man über das Lookup per findGetter() / findSetter() und findStaticGetter() / findStaticSetter() auch direkten Zugriff auf Felder. Das funktioniert, indem zur Laufzeit ein entsprechender Getter oder Setter automatisch erzeugt wird. Von invoke(…) gibt es noch andere Varianten, zum Beispiel invokeExact(…), das keine automatische Typumwandlung mehr durchführt und deswegen etwas effizienter ist.

CallSite

CallSites sind in Java als Klasse CallSite zwar direkt sichtbar, aber nicht in den Code als Methodenaufruf einbettbar. Das heißt in Java können die oben angesprochenen Optimierungen nicht direkt genutzt werden, sondern es muss auf die Ebene des Bytecodes herunter gegangen werden, um davon profitieren zu können, doch dazu später mehr. Es gibt drei Arten von CallSites: konstante, änderbare und volatile. Die ConstantCallSite benutzt immer das gleiche MethodHandle. Da sich das Ziel nie ändert, ändert sich der vom JIT generierte native Code für die CallSite nicht und der Methodenaufruf ist nach dem einmaligen Erzeugen der CallSite genauso effizient wie ein direkter Aufruf:

ConstantCallSite callSite = new ConstantCallSite(println);

Bei der MutableCallSite kann das MethodHandle geändert werden. Dazu wird es per setTarget(…) gesetzt und anschließend sollte man anderen Threads per syncAll(…) bekanntgegeben, dass sich das MethodHandle geändert hat. Solange sich das MethodHandle nicht ändert, ändert sich der vom JIT generierte native Code für die CallSite nicht und der Methodenaufruf ist genauso effizient wie ein direkter Aufruf:

// MutableCallSite mit initialem MethodHandle
MutableCallSite callSite = new MutableCallSite(println);
...
// Änderung des Ziels
callSite.setTarget(printlnNeu);
// Änderung für alle Threads sichtbar machen
MutableCallSite.syncAll(callSite)

Bei der VolatileCallSite wird davon ausgegangen, dass sich das MethodHandle so oft ändert, dass die explizite Bekanntgabe der Änderung per syncAll(…) wie bei der MutableCallSite zu viel Aufwand produzieren würde. Deswegen holt sich die VolatileCallSite bei jedem Aufruf das MethodHandle aus einem volatilen Feld. Da dies jegliche Optimierung verhindert, sollte die VolatileCallSite mit Bedacht eingesetzt werden:

// VolatileCallSite mit initialem MethodHandle
VolatileCallSite callSite = new VolatileCallSite(println);
...
// Änderung des Ziels
callSite.setTarget(printlnNeu1);
callSite.setTarget(printlnNeu2);
callSite.setTarget(printlnNeu3);

InvokeDynamic

Mit InvokeDynamic finden MethodHandle und CallSite zusammen. Es beschreibt im Bytecode eine CallSite. Die konkrete CallSite wird dynamisch über eine Bootstrap-Methode bestimmt, auf die der InvokeDynamic ein MethodHandle enthält. Listing 1 zeigt ein einfaches „Hallo Welt!“ mit InvokeDynamic, das mit dem Visitor-API von ASM 4 realisiert wurde (Klasse Simple1).

// Signatur der Bootstrap-Methode
MethodType bootstrapType = MethodType.methodType(
  CallSite.class, Lookup.class, String.class, MethodType.class);

// Handle zur Bootstrap-Methode (ASM-Äquivalent zu MethodHandle)
Handle bootstrapHandle = new Handle(
   H_INVOKESTATIC,          // statische Methode 
  Type.getType(getClass()).getInternalName(),  // an dieser Klasse
  "bootstrap",            // mit Namen "bootstrap"
  bootstrapType.toMethodDescriptorString());  // restliche Signatur

// Signatur der dynamisch aufzurufenden Methode: void dummy(String) an PrintStream
MethodType methodType = MethodType.methodType(
  void.class,            // Rückgabewert
  PrintStream.class,          // Typ von System.out
  String.class);          // 1. Parameter: String


// Die Methode, in der das InvokeDynamic enthalten ist...
MethodVisitor method = ...;

// System.out holen
method.visitFieldInsn(
  GETSTATIC, 
  Type.getType(System.class).getInternalName(), 
  "out", 
  Type.getType(PrintStream.class).getDescriptor());

// "Hallo Welt!" als 1. Parameter
method.visitLdcInsn("Hallo Welt!");

// Der eigentliche InvokeDynamic
method.visitInvokeDynamicInsn(
  "dummy",            // Name der Methode
  methodType.toMethodDescriptorString(),    // Signatur der Methode
  bootstrapHandle);        // Handle zur Bootstrap-Methode

Als Erstes wird hier per GETSTATIC System.out als aufzurufendes Objekt auf den Stack gelegt. Danach wird per Ldc die Konstante „Hallo Welt!“ als erster Parameter für die aufzurufende Methode auf den Stack gelegt. Jetzt kommt InvokeDynamic. Es enthält als erstes den Namen der aufzurufenden Methode. Der ist hier egal, da die Bootstrap-Methode aus diesem Beispiel (siehe unten) ihn später nicht berücksichtigen wird. Die Signatur (MethodType) der aufzurufenden Methode ist etwas unerwartet. Sie hat als ersten Parameter (nach dem Rückgabewert) die Klasse des aufzurufenden Objekts. Da InvokeDynamic nicht wirklich direkt eine Methode an diesem Objekt aufruft, ist die Semantik hier ähnlich des invoke(…)-Aufrufs bei Reflection. Man reicht das aufzurufende Objekt als erstes direkt vor den Parametern für die Methode rein. Als letztes bekommt der InvokeDynamic dann ein MethodHandle zur Bootstrap-Methode.

Beim ersten Ausführen eines InvokeDynamic wird die Bootstrap-Methode aufgerufen und gibt eine CallSite zurück, die in allen folgenden Ausführungen des InvokeDynamic direkt verwendet wird. Das heißt die Bootstrap-Methode wird pro InvokeDynamic nur einmal aufgerufen. Alle späteren Änderungen an der CallSite müssen dann auf anderen Wegen erfolgen. Ein Nachteil dieses dynamischen Linkens ist leider, dass es zur Laufzeit zu Fehlern kommen kann, wenn zum Beispiel die Bootstrap-Methode fehlerhaft ist und eine Exception wirft.

Die Bootstrap-Methode bekommt ein Lookup mit der Sicht des Aufrufers, sowie den Namen und den MethodType der aufgerufenen Methode. Da der Name der aufgerufenen Methode reingereicht wird, kann man eine Bootstrap-Methode durchaus für mehrere InvokeDynamics verwenden. Eine einfache Bootstrap-Methode zeigt das folgende Listing (Klasse Simple1).

public static CallSite bootstrap(Lookup caller, String name, MethodType type) throws Exception {
  MethodType printlnType = MethodType.methodType(void.class, String.class);
  MethodHandle target = caller.findVirtual(PrintStream.class, "println", printlnType);
  return new ConstantCallSite(target);
}

In dieser Beispielmethode wird ein MethodHandle zu PrintStream.println(String) geholt und in einer nicht veränderbaren CallSite zurückgegeben. Damit wird der obige InvokeDynamic fest auf PrintStream.println(String) gesetzt. Es besteht ab jetzt kein Unterschied mehr zwischen obigem InvokeDynamic und einem direkten Aufruf von PrintStream.println(String).

Der eben beschriebene Fall ist einfach, da die Signatur des Methodenaufrufs der Signatur der aufgerufenen Methode entspricht. Was ist aber, wenn die aufgerufene Methode Zugriff auf weitere Werte zum Beispiel aus dem Kontext des Methodenaufrufers benötigt wird? Dazu gibt es die Klasse MethodHandles, die ein MethodHandle ändern kann, genauer gesagt auf Basis eines MethodHandle ein neues erzeugt, da diese unveränderbar sind. In der Bootstrap-Methode in folgendem Listing wird zum Beispiel einem MethodHandle ein Aufrufparameter fest hinzugefügt (Klasse Simple2).

public static CallSite bootstrap(Lookup caller, String name, MethodType type) throws Exception {
  MethodType printlnType = MethodType.methodType(void.class, String.class);
  MethodHandle target = caller.findVirtual(
    PrintStream.class, "println", printlnType);
  target = MethodHandles.insertArguments(target, 1, "Hallo Welt!");
  return new ConstantCallSite(target);
}

Durch MethodHandles.insertArguments(…) wird hier ein neues MethodHandle erzeugt, das beim Aufruf einen Parameter weniger (!) benötigt, da dieser fest vorgegeben wird. Beim InvokeDynamic-Aufruf kann der String-Parameter aus dem ersten Beispiel entfallen. Es wurde also ein Methodenaufruf ohne Parameter geschaffen, der immer PrintStream.println(„Hallo Welt!“) aufruft. Diese Art der Manipulation von MethodHandles hat keinen negativen Einfluss auf die Ausführungsgeschwindigkeit, da MethodHandles von der JVM „verstanden“ werden und der ausgeführte Code im Prinzip so aussieht, als ob der Aufrufer das zusätzliche Argument selbst übergeben hätte.

Bis jetzt wurde zwar viel Dynamik gezeigt, doch wo steckt das Optimierungspotenzial? InvokeDynamic dient hauptsächlich dazu, den Aufwand zur Bestimmung der konkret aufzurufenden Methode nur einmal machen zu müssen, und bei jedem folgenden Aufruf diese Methode direkt aufzurufen. Dieses Ziel ist bereits jetzt erreicht. Zusätzlich wird durch dieses „Verstecken“ des Bestimmungsaufwands das Inlinen des Methodenaufrufs durch den JIT ermöglicht, was weitere Performancevorteile bringt.

Um noch besser optimieren zu können, sollen zusätzlich auch Spekulationen bezüglich der konkret aufzurufenden Methode möglich sein, die aber zur Laufzeit auf ihre Gültigkeit geprüft werden müssen. Zu diesem Zweck gibt es MethodHandles.guardWithTest(…). Diese Methode erzeugt ein MethodHandle, das abhängig von einer Gültigkeitsprüfung (Testmethode) entweder einen optimierten Pfad oder einen Fallback-Pfad ausführt.

Im nächsten Listing (Klasse Guard1) wird für das „Hallo Welt!“ eine Methode verwendet, die den PrintStream reingereicht bekommt, auf dem die Ausgabe erscheinen soll. Wenn es sich dabei um System.out handelt, soll eine „optimierte“ Implementierung verwendet werden. Sonst soll einfach am reingereichten PrintStream normal println(String) aufgerufen werden.

public static CallSite bootstrap(Lookup caller, String name, MethodType type) throws Exception {
  // CallSite erstmal ohne MethodHandle erzeugen
  MutableCallSite result = new MutableCallSite(type);

  // Eigenes Lookup verwenden, da caller die privaten Methoden nicht sehen kann
  Lookup lookup = MethodHandles.lookup();

  // MethodHandle zur Testmethode
  MethodType testType = MethodType.methodType(
    boolean.class, PrintStream.class, String.class);
  MethodHandle test = lookup.findStatic(Guard1.class, "test", testType);

  // MethodHandle zum "optimierten" println(String)
  MethodType targetType = MethodType.methodType(void.class, String.class);
  MethodHandle target = lookup.findStatic(Guard1.class, "println", targetType);
  // PrintStream ignorieren, da er hier nicht benötigt wird.
  // Fügt einen PrintStream in die Signatur ein(!), der dann ignoriert wird.
  target = MethodHandles.dropArguments(target, 0, PrintStream.class);

  // MethodHandle zum Fallback PrintStream.println(String)
  MethodType fallbackType = MethodType.methodType(void.class, String.class);
  MethodHandle fallback = caller.findVirtual(
    PrintStream.class, "println", fallbackType);

  MethodHandle guard = MethodHandles.guardWithTest(test, target, fallback);
  result.setTarget(guard);

  return result;
}

/**
 * Test-Methode, die prüft, ob auf System.out ausgegeben werden soll.
 *
 * @param stream Ausgabe-Stream
 * @param message Meldung
 * @return true, wenn es sich bei stream um System.out handelt
 */
private static boolean test(PrintStream stream, String message) {
  return stream == System.out;
}

/**
 * "Optimierte" println-Methode.
 *
 * @param message Meldung
 */
private static void println(String message) {
  ...
}

Der Guard in diesem Beispiel sorgt dafür, dass bei jedem Aufruf per Testmethode (über test) geprüft wird, ob die getroffene Annahme (dass es sich um System.out handelt) stimmt. Wenn ja, wird die optimierte Methode (über target) aufgerufen. Sonst wird die Fallback-Methode (über fallback) aufgerufen. Wichtig hierbei ist, dass alle drei MethodHandles (test, target und fallback) die gleiche Signatur (MethodType) haben, da sie alle direkt mit den Parametern, die vom InvokeDynamic kommen, aufgerufen werden. Einzige Ausnahme ist der Rückgabetyp der Testmethode, der fest boolean sein muss.

Dieses Beispiel zeigt das Optimierungspotenzial. Für den 90 %-Fall wird eine „optimierte“ Implementierung von println(String) zur Verfügung gestellt, die aber nur mit System.out funktioniert. Handelt es sich beim PrintStream nicht um System.out, wird einfach die ursprüngliche „langsame“ Methode am PrintStream aufgerufen. Dadurch kann optimiert werden, ohne jeglichen Spezialfall berücksichtigen zu müssen.

Wie man hier aber auch sieht, funktioniert die Optimierung nur, wenn es wenige zu optimierende Fälle gibt. Das heißt zum Beispiel, dass bei einer virtuellen Methode, die von sehr vielen Klassen überschrieben wird, deren Aufruf per InvokeDynamic nicht optimiert werden kann. Deswegen sollte man bei der Nutzung von InvokeDynamic darauf achten, dass man den umgebenden Code so gestaltet, dass diese Voraussetzung gegeben ist. Das heißt, dass man darauf achtet, nicht unnötig oft Methoden zu überschreiben, oder, dass von einem InvokeDynamic diese Methode möglichst nur an Objekten einer dieser Subklassen aufgerufen wird.

Eine andere Möglichkeit, die Fallback-Methode eines Guards zu gestalten, ist, die Optimierung den neuen Gegebenheiten anzupassen, also das Ziel der CallSite auf eine andere Methode umzubiegen. Zum Beispiel könnte man einfach die CallSite in der Fallback-Methode fest auf PrintStream.println(String) setzen und so die Optimierung wieder komplett entfernen. Dafür muss aber die CallSite der Fallback-Methode zur Verfügung stehen. Man könnte dafür einfach das MethodHandle zur Fallback-Methode per MethodHandles.insertArguments(…) um die CallSite ergänzen, wie das folgende Listing (Klasse Guard2) zeigt. In dieser Fallback-Methode wird zuerst die Optimierung entfernt, indem die CallSite auf PrintStream.println(String) gesetzt wird. Danach muss noch der eigentliche Methodenaufruf durchgeführt werden.

public static CallSite bootstrap(Lookup caller, String name, MethodType type) throws Exception {
  ... Wie im vorherigen Beispiel ...

  // MethodHandle zur Fallbackmethode
  MethodType fallbackType = MethodType.methodType(
    void.class, PrintStream.class, String.class, MutableCallSite.class);
  MethodHandle fallback = lookup.findStatic(
    Guard2.class, "fallback", fallbackType);
  // CallSite fest in Aufruf integrieren
  fallback = MethodHandles.insertArguments(fallback, 2, result);

  // Guard erzeugen
  MethodHandle guard = MethodHandles.guardWithTest(test, target, fallback);
  result.setTarget(guard);

  return result;
}

/**
 * Fallback-Methode.
 *
 * @param message Meldung
 * @param callSite CallSite
 */
private static void fallback(PrintStream stream, String message, MutableCallSite callSite) throws Throwable {
  // MethodHandle zu PrintStream.println(String)
  MethodType printlnType = MethodType.methodType(void.class, String.class);
  MethodHandle println = MethodHandles.publicLookup().findVirtual(
    PrintStream.class, "println", printlnType);

  // Optimierung entfernen
  callSite.setTarget(println);
  MutableCallSite.syncAll(new MutableCallSite[]{callSite});

  // Aufruf "nachholen"
  println.invoke(stream, message);
}

Wozu?

Wie ich gezeigt habe, ist InvokeDynamic ein technisches Feature, das nur per Bytecode-Generierung genutzt werden kann. Es kommt deswegen nicht zur Java-Anwendungsentwicklung infrage. Aber soll es das jetzt gewesen sein? Haben wirklich nur dynamische Sprachen etwas von diesem Feature? Wenn man es genau betrachtet, braucht Java auf den ersten Blick gar kein InvokeDynamic, da die anderen Invoke-Bytecodes die Optimierungen, die jetzt zur Verfügung stehen, sowieso schon nutzen. Zudem muss man sich dann nicht mal selber darum kümmern, da die JVM das von alleine macht.

Auf den zweiten Blick gibt es aber auch Anwendungsbereiche für InvokeDynamic in Java. Diese werden in Frameworks zu finden sein. Gerade bei Frameworks, die viel auf Reflection setzen, wird der Overhead durch Einsatz von MethodHandles (auch ohne InvokeDynamic) reduzierbar sein, da der Aufruf sofort mit der Performance eines normalen Methodenaufrufs stattfinden kann, ohne darauf warten zu müssen, dass der JIT den Code optimiert.

InvokeDynamic kann bei jeglicher Art der verzögerten (lazy) Initialisierung genutzt werden, damit der Overhead zur Prüfung, ob die Initialisierung bereits ausgeführt wurde, wegfällt. Außerdem behindert InvokeDynamic das Inlinen nicht, wie es manuelle Initialisierungen tun, wodurch der Zugriff auf das eigentliche Objekt nochmals beschleunigt wird. Ein einfaches Beispiel dafür ist ein Singleton, der dann so schnell wäre wie ein einfacher Feldzugriff, mit dem zusätzlichen Vorteil, dass die verzögerte Initialisierung auch ohne Synchronisierung bei Nebenläufigkeit korrekt funktionieren würde.

Der Kritikpunkt an InvokeDynamic, dass es keine Möglichkeit gibt, es per Java zu nutzen, wird wahrscheinlich mit Java 8 behoben sein. Das freut nicht nur den Java-Entwickler, der sich nicht mehr mit Bytecode quälen muss, sondern ist auch für Compiler-Entwickler interessant, da es einfacher ist, Java-Sourcecode als direkt Bytecode zu erzeugen. Dadurch würde die Einstiegshürde für den Bau von Compilern auf Basis der JVM weiter sinken.

Ein interessanter Beleg dafür, dass InvokeDynamic eine wegweisende Erweiterung der JVM ist, ist Facebook (neben der Performance der bereits InvokeDynamic nutzenden Sprachen, wie zum Beispiel JRuby). Facebook denkt Gerüchten zufolge ernsthaft darüber nach, auf die JVM zu wechseln [1]. Das heißt aber nicht, dass der vorhandene PHP-Quellcode auf Java umgestellt wird, sondern, dass ein PHP-Compiler auf Basis der JVM entstehen soll. Facebook tut das, um die Ausführungsgeschwindigkeit zu steigern, und das, obwohl sie bereits jetzt einen PHP-zu-C++-Compiler einsetzen. Es scheint also, dass InvokeDynamic sein Ziel erfüllt hat, wenn selbst eine große Plattform wie Facebook so einen Schritt wagen will.

Fazit

InvokeDynamic ist ein interessantes neues Feature der JVM, das das effiziente Implementieren von neuen Features für Java ermöglicht, die vorher nur mit großen Laufzeitnachteilen möglich waren. Allerdings ist auch immer das Erzeugen von Bytecode nötig, was einen Einsatz direkt in Java-Anwendungscode bisher ausschließt. Damit bleiben im Java-Kontext nur Frameworks übrig, die ihre Funktionalität über ein API bereitstellen, was aber kein Nachteil sein muss. Mit Java 8 wird dieser Kritikpunkt aber wahrscheinlich noch verschwinden.

Ansonsten ist InvokeDynamic von entscheidender Bedeutung für den Bau von Compilern für dynamische Sprachen auf der JVM, wofür es schließlich gebaut wurde. Mit Java 8 wird noch weiter an der Benutzbarkeit von InvokeDynamic gefeilt, sodass es zukünftig noch einfacher sein wird, neue Sprachen auf die JVM zu bringen.

Geschrieben von
Markus Heiden
Markus Heiden
  Markus Heiden ist seit über zehn Jahren Softwareentwickler und Berater bei der C1 WPS GmbH. Kontakt: Markus.Heiden@c1-wps.de.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.