Invokedynamic 101

In der Java 7-Version von Oracle wurde eine neue invokedynamicBytecode-Anweisung für die Java Virtual Machine (JVM) und ein neues java.lang.invokeAPI-Paket für die Standardklassenbibliothek eingeführt. Dieser Beitrag führt Sie in diese Anweisung und API ein.

Das Was und Wie von Invokedynamic

F: Was ist invokedynamic?

A: invokedynamic ist eine Bytecode-Anweisung, die die Implementierung dynamischer Sprachen (für die JVM) durch Aufrufen dynamischer Methoden erleichtert. Diese Anweisung ist in der Java SE 7 Edition der JVM-Spezifikation beschrieben.

Dynamische und statische Sprachen

Eine dynamische Sprache (auch als dynamisch typisierte Sprache bezeichnet ) ist eine Programmiersprache auf hoher Ebene, deren Typprüfung normalerweise zur Laufzeit durchgeführt wird. Diese Funktion wird als dynamische Typisierung bezeichnet . Die Typprüfung überprüft, ob ein Programm typsicher ist : Alle Operationsargumente haben den richtigen Typ. Groovy, Ruby und JavaScript sind Beispiele für dynamische Sprachen. (Die @groovy.transform.TypeCheckedAnnotation bewirkt, dass Groovy beim Kompilieren die Prüfung eingibt.)

Im Gegensatz dazu führt eine statische Sprache (auch als statisch typisierte Sprache bezeichnet ) zur Kompilierungszeit eine Typprüfung durch, eine Funktion, die als statische Typisierung bezeichnet wird . Der Compiler überprüft, ob ein Programm typrichtig ist, kann jedoch eine Typprüfung auf die Laufzeit verschieben (denken Sie an Casts und die checkcastAnweisung). Java ist ein Beispiel für eine statische Sprache. Der Java-Compiler verwendet diese Typinformationen, um einen stark typisierten Bytecode zu erzeugen, der von der JVM effizient ausgeführt werden kann.

F: Wie invokedynamicerleichtert sich die dynamische Sprachimplementierung?

A: In einer dynamischen Sprache erfolgt die Typprüfung normalerweise zur Laufzeit. Entwickler müssen geeignete Typen übergeben, da sonst Laufzeitfehler auftreten können. Dies ist häufig der java.lang.Objectgenaueste Typ für ein Methodenargument. Diese Situation erschwert die Typprüfung, was sich auf die Leistung auswirkt.

Eine weitere Herausforderung besteht darin, dass dynamische Sprachen normalerweise die Möglichkeit bieten, Felder / Methoden zu vorhandenen Klassen hinzuzufügen und aus diesen zu entfernen. Daher ist es erforderlich, die Klassen-, Methoden- und Feldauflösung auf die Laufzeit zu verschieben. Außerdem ist es häufig erforderlich, einen Methodenaufruf an ein Ziel mit einer anderen Signatur anzupassen.

Für diese Herausforderungen musste traditionell eine Ad-hoc-Laufzeitunterstützung auf der JVM aufgebaut werden. Diese Unterstützung umfasst Wrapper-Typklassen, die Hash-Tabellen verwenden, um eine dynamische Symbolauflösung bereitzustellen, und so weiter. Der Bytecode wird mit Einstiegspunkten in die Laufzeit in Form von Methodenaufrufen unter Verwendung einer der vier Anweisungen zum Aufrufen von Methoden generiert:

  • invokestaticwird zum Aufrufen von staticMethoden verwendet.
  • invokevirtualwird zum Aufrufen publicund protectedNicht- staticMethoden über den dynamischen Versand verwendet.
  • invokeinterfaceähnelt, invokevirtualaußer dass der Methodenversand auf einem Schnittstellentyp basiert.
  • invokespecialwird verwendet, um Instanzinitialisierungsmethoden (Konstruktoren) sowie privateMethoden und Methoden einer Oberklasse der aktuellen Klasse aufzurufen .

Diese Laufzeitunterstützung wirkt sich auf die Leistung aus. Der generierte Bytecode erfordert häufig mehrere tatsächliche Aufrufe von JVM-Methoden für einen Aufruf einer dynamischen Sprachmethode. Reflexion ist weit verbreitet und trägt zur Leistungsverschlechterung bei. Die vielen verschiedenen Ausführungspfade machen es dem Just-in-Time-Compiler (JIT) der JVM außerdem unmöglich, Optimierungen anzuwenden.

Um eine schlechte Leistung zu beheben, wird durch die invokedynamicAnweisung die Ad-hoc-Laufzeitunterstützung abgeschafft. Stattdessen werden beim ersten Aufruf Bootstraps durch Aufrufen der Laufzeitlogik ausgeführt, die eine Zielmethode effizient auswählt, und nachfolgende Aufrufe rufen normalerweise die Zielmethode auf, ohne dass ein erneuter Bootstrap erforderlich ist.

invokedynamicDies kommt auch Implementierern dynamischer Sprachen zugute, indem sie sich dynamisch ändernde Call-Site-Ziele unterstützen - eine Call-Site , insbesondere eine dynamische Call-Site, ist eine invokedynamicAnweisung. Da die JVM intern unterstützt invokedynamic, kann diese Anweisung vom JIT-Compiler besser optimiert werden.

Methodenhandles

F: Ich verstehe, dass dies invokedynamicmit Methodenhandles funktioniert, um den dynamischen Methodenaufruf zu erleichtern. Was ist ein Methodenhandle?

A: Ein Methodenhandle ist "eine typisierte, direkt ausführbare Referenz auf eine zugrunde liegende Methode, einen Konstruktor, ein Feld oder eine ähnliche Operation auf niedriger Ebene mit optionalen Transformationen von Argumenten oder Rückgabewerten". Mit anderen Worten, es ähnelt einem Funktionszeiger im C-Stil, der auf ausführbaren Code - ein Ziel - verweist und der zum Aufrufen dieses Codes dereferenziert werden kann. Methodenhandles werden von der abstrakten java.lang.invoke.MethodHandleKlasse beschrieben.

F: Können Sie ein einfaches Beispiel für die Erstellung und den Aufruf von Methodenhandles angeben?

A: Schauen Sie sich Listing 1 an.

Listing 1. MHD.java(Version 1)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findStatic(MHD.class, "hello", MethodType.methodType(void.class)); mh.invokeExact(); } static void hello() { System.out.println("hello"); } }

Listing 1 beschreibt ein Demonstrationsprogramm für Methodenhandles, das aus main()und hello()Klassenmethoden besteht. Ziel dieses Programms ist es, hello()über ein Methodenhandle aufzurufen .

main()Die erste Aufgabe besteht darin, ein java.lang.invoke.MethodHandles.LookupObjekt zu erhalten . Dieses Objekt ist eine Factory zum Erstellen von Methodenhandles und wird zum Suchen nach Zielen wie virtuellen Methoden, statischen Methoden, speziellen Methoden, Konstruktoren und Feldzugriffsmethoden verwendet. Darüber hinaus hängt es vom Aufrufkontext einer Aufrufsite ab und erzwingt bei jeder Erstellung eines Methodenhandles Zugriffsbeschränkungen für Methodenhandles. Mit anderen Worten, eine Anrufstelle (wie die main()Methode von Listing 1, die als Anrufstelle fungiert), die ein Suchobjekt erhält, kann nur auf die Ziele zugreifen, auf die die Anrufstelle zugreifen kann. Das Suchobjekt wird durch Aufrufen der Methode der java.lang.invoke.MethodHandlesKlasse erhalten MethodHandles.Lookup lookup().

publicLookup()

MethodHandlesdeklariert auch eine MethodHandles.Lookup publicLookup()Methode. Im Gegensatz dazu lookup()kann publicLookup()ein Methodenhandle nur für ein öffentlich zugängliches Feld oder nur für ein öffentlich zugängliches Verfahren / Konstruktor abgerufen werden.

Nach dem Abrufen des Suchobjekts wird die MethodHandle findStatic(Class refc, String name, MethodType type)Methode dieses Objekts aufgerufen, um ein Methodenhandle für die hello()Methode abzurufen. Das erste übergebene Argument findStatic()ist ein Verweis auf die Klasse ( MHD), von der aus auf method ( hello()) zugegriffen wird, und das zweite Argument ist der Name der Methode. Das dritte Argument ist ein Beispiel für einen Methodentyp , der "die Argumente und den Rückgabetyp darstellt, die von einem Methodenhandle akzeptiert und zurückgegeben werden, oder die Argumente und den Rückgabetyp, die von einem Aufrufer eines Methodenhandles übergeben und erwartet werden". Es wird durch eine Instanz der java.lang.invoke.MethodTypeKlasse dargestellt und (in diesem Beispiel) durch Aufrufen java.lang.invoke.MethodTypeder MethodType methodType(Class rtype)Methode von erhalten. Diese Methode wird aufgerufen, da hello()nur ein Rückgabetyp bereitgestellt wirdvoid. Dieser Rückgabetyp wird methodType()durch Übergabe void.classan diese Methode verfügbar gemacht .

Das zurückgegebene Methodenhandle ist zugewiesen mh. Dieses Objekt wird dann auf Aufruf verwendet MethodHandles Object invokeExact(Object... args)Methode, die Methode Griff aufzurufen. Mit anderen Worten invokeExact()ergeben hello()genannt zu werden, und helloauf die Standardausgabe geschrieben werden. Da invokeExact()deklariert ist zu werfen Throwable, habe ich an throws Throwableden main()Methodenheader angehängt .

F: In Ihrer vorherigen Antwort haben Sie erwähnt, dass das Suchobjekt nur auf die Ziele zugreifen kann, auf die die Anrufsite zugreifen kann. Können Sie ein Beispiel angeben, das zeigt, wie versucht wird, ein Methodenhandle für ein unzugängliches Ziel zu erhalten?

A: Schauen Sie sich Listing 2 an.

Listing 2. MHD.java(Version 2)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class HW { public void hello1() { System.out.println("hello from hello1"); } private void hello2() { System.out.println("hello from hello2"); } } public class MHD { public static void main(String[] args) throws Throwable { HW hw = new HW(); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(HW.class, "hello1", MethodType.methodType(void.class)); mh.invoke(hw); mh = lookup.findVirtual(HW.class, "hello2", MethodType.methodType(void.class)); } }

Listing 2 deklariert HW(Hallo, Welt) und MHDKlassen. HWdeklariert eine publichello1()Instanzmethode und eine privatehello2()Instanzmethode. MHDdeklariert eine main()Methode, die versucht, diese Methoden aufzurufen.

main()Die erste Aufgabe besteht darin, HWin Vorbereitung auf das Aufrufen von hello1()und zu instanziieren hello2(). Als Nächstes erhält es ein Suchobjekt und verwendet dieses Objekt, um ein Methodenhandle zum Aufrufen zu erhalten hello1(). Diesmal wird MethodHandles.Lookupdie findVirtual()Methode aufgerufen, und das erste an diese Methode übergebene Argument ist ein ClassObjekt, das die HWKlasse beschreibt.

Es stellt sich heraus, dass dies findVirtual()erfolgreich sein wird und der nachfolgende mh.invoke(hw);Ausdruck aufgerufen wird hello1(), was hello from hello1zur Ausgabe führt.

Da hello1()ist public, dann ist es an der zugänglichen main()Methodenaufruf Website. Im Gegensatz dazu hello2()ist nicht zugänglich. Infolgedessen schlägt der zweite findVirtual()Aufruf mit einem fehl IllegalAccessException.

Wenn Sie diese Anwendung ausführen, sollten Sie die folgende Ausgabe beachten:

hello from hello1 Exception in thread "main" java.lang.IllegalAccessException: member is private: HW.hello2()void, from MHD at java.lang.invoke.MemberName.makeAccessException(MemberName.java:507) at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1172) at java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:1152) at java.lang.invoke.MethodHandles$Lookup.accessVirtual(MethodHandles.java:648) at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:641) at MHD.main(MHD.java:27)

F: In den Listen 1 und 2 werden die Methoden invokeExact()und invoke()verwendet, um ein Methodenhandle auszuführen. Was ist der Unterschied zwischen diesen Methoden?

A: Obwohl invokeExact()und invoke()für die Ausführung eines Methodenhandles (eigentlich des Zielcodes, auf den sich das Methodenhandle bezieht) ausgelegt sind, unterscheiden sie sich bei der Durchführung von Typkonvertierungen für Argumente und den Rückgabewert. invokeExact()führt keine automatische Konvertierung vom kompatiblen Typ für Argumente durch. Seine Argumente (oder Argumentausdrücke) müssen exakt vom Typ mit der Methodensignatur übereinstimmen, wobei jedes Argument separat oder alle Argumente zusammen als Array bereitgestellt werden. invoke()erfordert, dass seine Argumente (oder Argumentausdrücke) typkompatibel mit der Methodensignatur übereinstimmen - automatische Typkonvertierungen werden durchgeführt, wobei jedes Argument separat oder alle Argumente zusammen als Array bereitgestellt werden.

F: Können Sie mir ein Beispiel geben, das zeigt, wie der Getter und Setter eines Instanzfelds aufgerufen wird?

A: Schauen Sie sich Listing 3 an.

Listing 3. MHD.java(Version 3)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class Point { int x; int y; } public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); Point point = new Point(); // Set the x and y fields. MethodHandle mh = lookup.findSetter(Point.class, "x", int.class); mh.invoke(point, 15); mh = lookup.findSetter(Point.class, "y", int.class); mh.invoke(point, 30); mh = lookup.findGetter(Point.class, "x", int.class); int x = (int) mh.invoke(point); System.out.printf("x = %d%n", x); mh = lookup.findGetter(Point.class, "y", int.class); int y = (int) mh.invoke(point); System.out.printf("y = %d%n", y); } }

Listing 3 führt eine PointKlasse mit einem Paar von 32-Bit-Ganzzahlinstanzfeldern mit dem Namen xund ein y. Jedes Feld der Setter und Getter wird durch Aufrufen zugegriffen MethodHandles.Lookup‚s findSetter()und findGetter()Methoden und die so erhaltenen MethodHandlezurückgeführt wird. Jedes von findSetter()und findGetter()erfordert ein ClassArgument, das die Klasse des Feldes, den Namen des Feldes und ein ClassObjekt identifiziert, das die Signatur des Feldes identifiziert.

Die invoke()Methode wird verwendet, um einen Setter oder Getter auszuführen - hinter den Kulissen wird auf die Instanzfelder über die JVMs putfieldund getfieldAnweisungen zugegriffen . Diese Methode erfordert, dass als erstes Argument ein Verweis auf das Objekt übergeben wird, auf dessen Feld zugegriffen wird. Bei Setter-Aufrufen muss auch ein zweites Argument übergeben werden, das aus dem dem Feld zugewiesenen Wert besteht.

Wenn Sie diese Anwendung ausführen, sollten Sie die folgende Ausgabe beachten:

x = 15 y = 30

F: Ihre Definition des Methodenhandles enthält den Ausdruck "mit optionalen Transformationen von Argumenten oder Rückgabewerten". Können Sie ein Beispiel für die Argumenttransformation geben?

A: Ich habe ein Beispiel basierend auf der MathKlassenmethode der double pow(double a, double b)Klasse erstellt. In diesem Beispiel erhalte ich ein Methodenhandle für die pow()Methode und transformiere dieses Methodenhandle so, dass das zweite übergebene Argument pow()immer lautet 10. Check out Listing 4.