Un'introduzione a Invoke Dynamic nella JVM

1. Panoramica

Invoke Dynamic (noto anche come Indy) faceva parte di JSR 292 inteso a migliorare il supporto JVM per linguaggi tipizzati dinamicamente. Dopo il suo primo rilascio in Java 7, il codice operativo invocato dinamico viene utilizzato in modo abbastanza esteso da linguaggi dinamici basati su JVM come JRuby e persino linguaggi di tipo statico come Java.

In questo tutorial, demistificheremo invokedynamic e vedremo come puòaiutare i progettisti di librerie e linguaggi a implementare molte forme di dinamicità.

2. Incontra Invoke Dynamic

Cominciamo con una semplice catena di chiamate API Stream:

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

All'inizio, potremmo pensare che Java crei una classe interna anonima derivante da Predicate e quindi passi quell'istanza al metodo di filtro . Ma ci sbaglieremmo.

2.1. Il Bytecode

Per verificare questa ipotesi, possiamo dare un'occhiata al bytecode generato:

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

Nonostante quello che pensavamo, non esiste una classe interna anonima e certamente nessuno sta passando un'istanza di tale classe al metodo di filtro .

Sorprendentemente, l' istruzione dinamica invocata è in qualche modo responsabile della creazione dell'istanza Predicate .

2.2. Metodi specifici per Lambda

Inoltre, il compilatore Java ha anche generato il seguente metodo statico dall'aspetto divertente:

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

Questo metodo accetta una stringa come input e quindi esegue i seguenti passaggi:

  • Calcolo della lunghezza di input (invokevirtual on length )
  • Confronto della lunghezza con la costante 3 ( if_icmple e iconst_3 )
  • Restituisce false se la lunghezza è minore o uguale a 3

È interessante notare che questo è in realtà equivalente al lambda che abbiamo passato al metodo di filtro :

c -> c.length() > 3

Quindi, invece di una classe interna anonima, Java crea uno speciale metodo statico e in qualche modo richiama quel metodo tramite invokedynamic.

Nel corso di questo articolo, vedremo come funziona internamente questa invocazione. Ma prima definiamo il problema che invokedynamic sta cercando di risolvere.

2.3. Il problema

Prima di Java 7, la JVM aveva solo quattro tipi di invocazione dei metodi: invokevirtual per chiamare i normali metodi di classe, invokestatic per chiamare metodi statici, invokeinterface per chiamare metodi di interfaccia e invokespecial per chiamare costruttori o metodi privati.

Nonostante le loro differenze, tutte queste invocazioni condividono una semplice caratteristica: hanno alcuni passaggi predefiniti per completare ogni chiamata al metodo e non possiamo arricchire questi passaggi con i nostri comportamenti personalizzati.

Esistono due soluzioni alternative principali per questa limitazione: una in fase di compilazione e l'altra in fase di esecuzione. Il primo è solitamente utilizzato da linguaggi come Scala o Koltin e il secondo è la soluzione preferita per i linguaggi dinamici basati su JVM come JRuby.

L'approccio runtime è solitamente basato sulla riflessione e, di conseguenza, inefficiente.

D'altra parte, la soluzione in fase di compilazione si basa solitamente sulla generazione di codice in fase di compilazione. Questo approccio è più efficiente in fase di esecuzione. Tuttavia, è un po 'fragile e può anche causare un tempo di avvio più lento poiché c'è più bytecode da elaborare.

Ora che abbiamo una migliore comprensione del problema, vediamo come funziona internamente la soluzione.

3. Sotto il cofano

invokedynamic ci consente di eseguire il bootstrap del processo di invocazione del metodo in qualsiasi modo desideriamo . Cioè, quando la JVM vede uncodice operativo dinamico invocato per la prima volta, chiama un metodo speciale noto come metodo bootstrap per inizializzare il processo di invocazione:

Il metodo bootstrap è un normale pezzo di codice Java che abbiamo scritto per impostare il processo di invocazione. Pertanto, può contenere qualsiasi logica.

Una volta che il metodo bootstrap viene completato normalmente, dovrebbe restituire un'istanza di CallSite. Questo CallSite incapsula le seguenti informazioni:

  • Un puntatore alla logica effettiva che JVM dovrebbe eseguire. Questo dovrebbe essere rappresentato come un MethodHandle.
  • Una condizione che rappresenta la validità del CallSite restituito .

D'ora in poi, ogni volta che JVM vede di nuovo questo particolare codice operativo, salterà il percorso lento e chiamerà direttamente l'eseguibile sottostante . Inoltre, la JVM continuerà a saltare il percorso lento finché la condizione nel CallSite non cambia.

Al contrario dell'API di Reflection, la JVM può vedere completamente i MethodHandle e tenterà di ottimizzarli, quindi le migliori prestazioni.

3.1. Tabella del metodo Bootstrap

Diamo un'altra occhiata al bytecode invocato dinamico generato :

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

Ciò significa che questa particolare istruzione dovrebbe chiamare il primo metodo bootstrap (parte # 0) dalla tabella del metodo bootstrap. Inoltre, menziona alcuni degli argomenti da passare al metodo bootstrap:

  • 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

Prima di Java 9, le concatenazioni di stringhe non banali venivano implementate utilizzando StringBuilder. Come parte di JEP 280, la concatenazione di stringhe ora utilizza invokedynamic. Ad esempio, concateniamo una stringa costante con una variabile casuale:

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

Ecco come appare il bytecode per questo esempio:

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

Inoltre, i metodi bootstrap per le concatenazioni di stringhe risiedono nella classe StringConcatFactory :

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. conclusione

In questo articolo, per prima cosa, abbiamo familiarizzato con i problemi che l'indy sta cercando di risolvere.

Quindi, esaminando un semplice esempio di espressione lambda, abbiamo visto come funziona internamente invokedynamic .

Infine, abbiamo elencato alcuni altri esempi di indy nelle versioni recenti di Java.