Eine Einführung zum Aufrufen von Dynamic in der JVM

1. Übersicht

Invoke Dynamic (auch als Indy bekannt) war Teil von JSR 292, um die JVM-Unterstützung für dynamisch typisierte Sprachen zu verbessern. Nach seiner ersten Veröffentlichung in Java 7 wird der aufgerufene dynamische Opcode von dynamischen JVM-basierten Sprachen wie JRuby und sogar statisch typisierten Sprachen wie Java ziemlich häufig verwendet.

In diesem Tutorial werden wir invokedynamic entmystifizieren und sehen, wie es gehthelfen Bibliotheks- und Sprachdesignern, viele Formen der Dynamik zu implementieren.

2. Treffen Sie Invoke Dynamic

Beginnen wir mit einer einfachen Kette von Stream-API-Aufrufen:

public class Main { public static void main(String[] args) { long lengthyColors = List.of("Red", "Green", "Blue") .stream().filter(c -> c.length() > 3).count(); } }

Zuerst könnten wir denken, dass Java eine anonyme innere Klasse erstellt, die von Predicate abgeleitet ist, und diese Instanz dann an die Filtermethode übergibt . Aber wir würden uns irren.

2.1. Der Bytecode

Um diese Annahme zu überprüfen, können wir einen Blick auf den generierten Bytecode werfen:

javap -c -p Main // truncated // class names are simplified for the sake of brevity // for instance, Stream is actually java/util/stream/Stream 0: ldc #7 // String Red 2: ldc #9 // String Green 4: ldc #11 // String Blue 6: invokestatic #13 // InterfaceMethod List.of:(LObject;LObject;)LList; 9: invokeinterface #19, 1 // InterfaceMethod List.stream:()LStream; 14: invokedynamic #23, 0 // InvokeDynamic #0:test:()LPredicate; 19: invokeinterface #27, 2 // InterfaceMethod Stream.filter:(LPredicate;)LStream; 24: invokeinterface #33, 1 // InterfaceMethod Stream.count:()J 29: lstore_1 30: return

Trotz allem , was wir dachten, es gibt keine anonyme innere Klasse und sicherlich wird niemand Geben eine Instanz eines solchen Klasse an die Filtermethode .

Überraschenderweise ist die aufgerufene dynamische Anweisung irgendwie für die Erstellung der Prädikatinstanz verantwortlich .

2.2. Lambda-spezifische Methoden

Zusätzlich hat der Java-Compiler die folgende komisch aussehende statische Methode generiert:

private static boolean lambda$main$0(java.lang.String); Code: 0: aload_0 1: invokevirtual #37 // Method java/lang/String.length:()I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Diese Methode verwendet einen String als Eingabe und führt dann die folgenden Schritte aus:

  • Berechnen der Eingabelänge (invokevirtual on length )
  • Vergleich der Länge mit der Konstante 3 ( if_icmple und iconst_3 )
  • Rückgabe von false, wenn die Länge kleiner oder gleich 3 ist

Interessanterweise entspricht dies tatsächlich dem Lambda, das wir an die Filtermethode übergeben haben :

c -> c.length() > 3

Anstelle einer anonymen inneren Klasse erstellt Java eine spezielle statische Methode und ruft diese Methode über invokedynamic auf.

Im Verlauf dieses Artikels werden wir sehen, wie dieser Aufruf intern funktioniert. Aber zuerst definieren wir das Problem, das invokedynamic zu lösen versucht.

2.3. Das Problem

Vor Java 7 hatte die JVM nur vier Methodenaufruftypen: invokevirtual zum Aufrufen normaler Klassenmethoden, invokestatic zum Aufrufen statischer Methoden, invokeinterface zum Aufrufen von Schnittstellenmethoden und invokepecial zum Aufrufen von Konstruktoren oder privaten Methoden.

Trotz ihrer Unterschiede haben alle diese Aufrufe eine einfache Eigenschaft: Sie haben einige vordefinierte Schritte, um jeden Methodenaufruf abzuschließen, und wir können diese Schritte nicht mit unseren benutzerdefinierten Verhaltensweisen bereichern.

Es gibt zwei Hauptumgehungen für diese Einschränkung: eine zur Kompilierungszeit und die andere zur Laufzeit. Ersteres wird normalerweise von Sprachen wie Scala oder Koltin verwendet, und letzteres ist die Lösung der Wahl für JVM-basierte dynamische Sprachen wie JRuby.

Der Laufzeitansatz ist normalerweise reflexionsbasiert und folglich ineffizient.

Andererseits basiert die Lösung zur Kompilierungszeit normalerweise auf der Codegenerierung zur Kompilierungszeit. Dieser Ansatz ist zur Laufzeit effizienter. Es ist jedoch etwas spröde und kann auch zu einer langsameren Startzeit führen, da mehr Bytecode verarbeitet werden muss.

Nachdem wir das Problem besser verstanden haben, wollen wir sehen, wie die Lösung intern funktioniert.

3. Unter der Haube

Mit invokedynamic können wir den Methodenaufrufprozess nach Belieben booten . Das heißt, wenn die JVMzum ersten Maleinen aufgerufenen dynamischen Opcodesieht, ruft sie eine spezielle Methode auf, die als Bootstrap-Methode bezeichnet wird, um den Aufrufprozess zu initialisieren:

Die Bootstrap-Methode ist ein normaler Java-Code, den wir zum Einrichten des Aufrufprozesses geschrieben haben. Daher kann es jede Logik enthalten.

Sobald die Bootstrap-Methode normal abgeschlossen ist, sollte sie eine Instanz von CallSite zurückgeben. Diese CallSite enthält die folgenden Informationen:

  • Ein Zeiger auf die tatsächliche Logik, die JVM ausführen soll. Dies sollte als MethodHandle dargestellt werden.
  • Eine Bedingung, die die Gültigkeit der zurückgegebenen CallSite darstellt.

Von nun an überspringt JVM jedes Mal, wenn dieser bestimmte Opcode erneut angezeigt wird, den langsamen Pfad und ruft die zugrunde liegende ausführbare Datei direkt auf . Darüber hinaus überspringt die JVM weiterhin den langsamen Pfad, bis sich die Bedingung auf der CallSite ändert.

Im Gegensatz zur Reflection-API kann die JVM MethodHandles vollständig durchschauen und versucht, sie zu optimieren, um die Leistung zu verbessern.

3.1. Bootstrap-Methodentabelle

Schauen wir uns den generierten aufgerufenen dynamischen Bytecode noch einmal an:

14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;

Dies bedeutet, dass diese spezielle Anweisung die erste Bootstrap-Methode (Teil # 0) aus der Bootstrap-Methodentabelle aufrufen sollte. Außerdem werden einige der Argumente erwähnt, die an die Bootstrap-Methode übergeben werden sollen:

  • The test is the only abstract method in the Predicate
  • The ()Ljava/util/function/Predicate represents a method signature in the JVM – the method takes nothing as input and returns an instance of the Predicate interface

In order to see the bootstrap method table for the lambda example, we should pass -v option to javap:

javap -c -p -v Main // truncated // added new lines for brevity BootstrapMethods: 0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory: (Ljava/lang/invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/MethodType; Ljava/lang/invoke/MethodType; Ljava/lang/invoke/MethodHandle; Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #62 (Ljava/lang/Object;)Z #64 REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z #67 (Ljava/lang/String;)Z

The bootstrap method for all lambdas is the metafactory static method in the LambdaMetafactory class.

Similar to all other bootstrap methods, this one takes at least three arguments as follows:

  • The Ljava/lang/invoke/MethodHandles$Lookup argument represents the lookup context for the invokedynamic
  • The Ljava/lang/String represents the method name in the call site – in this example, the method name is test
  • The Ljava/lang/invoke/MethodType is the dynamic method signature of the call site – in this case, it's ()Ljava/util/function/Predicate

In addition to these three arguments, bootstrap methods also can optionally accept one or more extra parameters. In this example, these are the extra ones:

  • The (Ljava/lang/Object;)Z is an erased method signature accepting an instance of Object and returning a boolean.
  • The REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z is the MethodHandle pointing to the actual lambda logic.
  • The (Ljava/lang/String;)Z is a non-erased method signature accepting one String and returning a boolean.

Put simply, the JVM will pass all the required information to the bootstrap method. Bootstrap method will, in turn, use that information to create an appropriate instance of Predicate. Then, the JVM will pass that instance to the filter method.

3.2. Different Types of CallSites

Once the JVM sees invokedynamic in this example for the first time, it calls the bootstrap method. As of writing this article, the lambda bootstrap method will use the InnerClassLambdaMetafactoryto generate an inner class for the lambda at runtime.

Then the bootstrap method encapsulates the generated inner class inside a special type of CallSite known as ConstantCallSite. This type of CallSite would never change after setup. Therefore, after the first setup for each lambda, the JVM will always use the fast path to directly call the lambda logic.

Although this is the most efficient type of invokedynamic, it's certainly not the only available option. As a matter of fact, Java provides MutableCallSite and VolatileCallSite to accommodate for more dynamic requirements.

3.3. Advantages

So, in order to implement lambda expressions, instead of creating anonymous inner classes at compile-time, Java creates them at runtime via invokedynamic.

One might argue against deferring inner class generation until runtime. However, the invokedynamic approach has a few advantages over the simple compile-time solution.

First, the JVM does not generate the inner class until the first use of lambda. Hence, we won't pay for the extra footprint associated with the inner class before the first lambda execution.

Additionally, much of the linkage logic is moved out from the bytecode to the bootstrap method. Therefore, the invokedynamic bytecode is usually much smaller than alternative solutions. The smaller bytecode can boost startup speed.

Suppose a newer version of Java comes with a more efficient bootstrap method implementation. Then our invokedynamic bytecode can take advantage of this improvement without recompiling. This way we can achieve some sort of forwarding binary compatibility. Basically, we can switch between different strategies without recompilation.

Finally, writing the bootstrap and linkage logic in Java is usually easier than traversing an AST to generate a complex piece of bytecode. So, invokedynamic can be (subjectively) less brittle.

4. More Examples

Lambda expressions are not the only feature, and Java is not certainly the only language using invokedynamic. In this section, we're going to get familiar with a few other examples of dynamic invocation.

4.1. Java 14: Records

Records are a new preview feature in Java 14 providing a nice concise syntax to declare classes that are supposed to be dumb data holders.

Here's a simple record example:

public record Color(String name, int code) {}

Given this simple one-liner, Java compiler generates appropriate implementations for accessor methods, toString, equals, and hashcode.

In order to implement toString, equals, or hashcode, Java is using invokedynamic. For instance, the bytecode for equals is as follows:

public final boolean equals(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokedynamic #27, 0 // InvokeDynamic #0:equals:(LColor;Ljava/lang/Object;)Z 7: ireturn

The alternative solution is to find all record fields and generate the equals logic based on those fields at compile-time. The more we have fields, the lengthier the bytecode.

On the contrary, Java calls a bootstrap method to link the appropriate implementation at runtime. Therefore, the bytecode length would remain constant regardless of the number of fields.

Looking more closely at the bytecode shows that the bootstrap method is ObjectMethods#bootstrap:

BootstrapMethods: 0: #42 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap: (Ljava/lang/invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/TypeDescriptor; Ljava/lang/Class; Ljava/lang/String; [Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; Method arguments: #8 Color #49 name;code #51 REF_getField Color.name:Ljava/lang/String; #52 REF_getField Color.code:I

4.2. Java 9: String Concatenation

Vor Java 9 wurden nicht triviale String-Verkettungen mit StringBuilder implementiert . Als Teil von JEP 280 verwendet die Zeichenfolgenverkettung jetzt invokedynamic. Lassen Sie uns zum Beispiel eine konstante Zeichenfolge mit einer Zufallsvariablen verketten:

"random-" + ThreadLocalRandom.current().nextInt();

So sieht der Bytecode für dieses Beispiel aus:

0: invokestatic #7 // Method ThreadLocalRandom.current:()LThreadLocalRandom; 3: invokevirtual #13 // Method ThreadLocalRandom.nextInt:()I 6: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)LString;

Darüber hinaus befinden sich die Bootstrap-Methoden für String-Verkettungen in der StringConcatFactory- Klasse:

BootstrapMethods: 0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants: (Ljava/lang/invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/MethodType; Ljava/lang/String; [Ljava/lang/Object;)Ljava/lang/invoke/CallSite; Method arguments: #36 random-\u0001

5. Schlussfolgerung

In diesem Artikel haben wir uns zunächst mit den Problemen vertraut gemacht, die der Indy zu lösen versucht.

Dann haben wir durch ein einfaches Lambda-Ausdrucksbeispiel gesehen, wie invokedynamic intern funktioniert.

Schließlich haben wir einige andere Beispiele für Indy in neueren Versionen von Java aufgelistet.