Invokedynamic 101
In der Java 7-Version von Oracle wurde eine neue invokedynamic
Bytecode-Anweisung für die Java Virtual Machine (JVM) und ein neues java.lang.invoke
API-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.TypeChecked
Annotation 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 checkcast
Anweisung). 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 invokedynamic
erleichtert 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.Object
genaueste 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:
invokestatic
wird zum Aufrufen vonstatic
Methoden verwendet.invokevirtual
wird zum Aufrufenpublic
undprotected
Nicht-static
Methoden über den dynamischen Versand verwendet.invokeinterface
ähnelt,invokevirtual
außer dass der Methodenversand auf einem Schnittstellentyp basiert.invokespecial
wird verwendet, um Instanzinitialisierungsmethoden (Konstruktoren) sowieprivate
Methoden 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 invokedynamic
Anweisung 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.
invokedynamic
Dies 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 invokedynamic
Anweisung. Da die JVM intern unterstützt invokedynamic
, kann diese Anweisung vom JIT-Compiler besser optimiert werden.
Methodenhandles
F: Ich verstehe, dass dies invokedynamic
mit 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.MethodHandle
Klasse 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.Lookup
Objekt 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.MethodHandles
Klasse erhalten MethodHandles.Lookup lookup()
.
publicLookup()
MethodHandles
deklariert 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.MethodType
Klasse dargestellt und (in diesem Beispiel) durch Aufrufen java.lang.invoke.MethodType
der 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.class
an diese Methode verfügbar gemacht .
Das zurückgegebene Methodenhandle ist zugewiesen mh
. Dieses Objekt wird dann auf Aufruf verwendet MethodHandle
s Object invokeExact(Object... args)
Methode, die Methode Griff aufzurufen. Mit anderen Worten invokeExact()
ergeben hello()
genannt zu werden, und hello
auf die Standardausgabe geschrieben werden. Da invokeExact()
deklariert ist zu werfen Throwable
, habe ich an throws Throwable
den 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 MHD
Klassen. HW
deklariert eine public
hello1()
Instanzmethode und eine private
hello2()
Instanzmethode. MHD
deklariert eine main()
Methode, die versucht, diese Methoden aufzurufen.
main()
Die erste Aufgabe besteht darin, HW
in 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.Lookup
die findVirtual()
Methode aufgerufen, und das erste an diese Methode übergebene Argument ist ein Class
Objekt, das die HW
Klasse beschreibt.
Es stellt sich heraus, dass dies findVirtual()
erfolgreich sein wird und der nachfolgende mh.invoke(hw);
Ausdruck aufgerufen wird hello1()
, was hello from hello1
zur 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 Point
Klasse mit einem Paar von 32-Bit-Ganzzahlinstanzfeldern mit dem Namen x
und ein y
. Jedes Feld der Setter und Getter wird durch Aufrufen zugegriffen MethodHandles.Lookup
‚s findSetter()
und findGetter()
Methoden und die so erhaltenen MethodHandle
zurückgeführt wird. Jedes von findSetter()
und findGetter()
erfordert ein Class
Argument, das die Klasse des Feldes, den Namen des Feldes und ein Class
Objekt 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 putfield
und getfield
Anweisungen 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 Math
Klassenmethode 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.