Pengenalan untuk Memohon Dinamik dalam JVM

1. Gambaran keseluruhan

Invoke Dynamic (Juga dikenali sebagai Indy) adalah sebahagian daripada JSR 292 yang bertujuan untuk meningkatkan sokongan JVM untuk bahasa yang dinaip secara dinamik. Setelah dilancarkan pertama kali di Java 7, opcodeynamicynamic digunakan secara meluas oleh bahasa berasaskan JVM dinamik seperti JRuby dan juga bahasa yang ditaip secara statik seperti Java.

Dalam tutorial ini, kita akan membongkar invokedynamic dan melihat bagaimana ia dapat dilakukanmembantu pereka perpustakaan dan bahasa untuk melaksanakan pelbagai bentuk dinamik.

2. Memenuhi Invoke Dinamik

Mari kita mulakan dengan rangkaian panggilan API Stream yang ringkas:

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

Pada mulanya, kami mungkin berpendapat bahawa Java membuat kelas dalaman tanpa nama yang berasal dari Predicate dan kemudian meneruskan instance tersebut ke kaedah filter . Tetapi, kita akan salah.

2.1. Kod Bytec

Untuk memeriksa andaian ini, kita dapat melihat kod bytec yang dihasilkan:

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

Walau apa yang kami fikirkan, tidak ada kelas dalaman tanpa nama dan pastinya, tidak ada yang menyampaikan contoh kelas sedemikian kepada kaedah penapis .

Anehnya, arahan invokedynamic entah bagaimana bertanggungjawab untuk membuat contoh Predicate .

2.2. Kaedah Khusus Lambda

Selain itu, penyusun Java juga menghasilkan kaedah statik yang kelihatan lucu berikut:

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

Kaedah ini mengambil String sebagai input dan kemudian melakukan langkah-langkah berikut:

  • Pengiraan panjang input (invokevirtual pada panjang )
  • Membandingkan panjang dengan pemalar 3 ( if_icmple dan iconst_3 )
  • Mengembalikan palsu jika panjangnya kurang dari atau sama dengan 3

Menariknya, ini sebenarnya setara dengan lambda yang kami lalui kaedah penapis :

c -> c.length() > 3

Oleh itu, bukannya kelas dalaman tanpa nama, Java membuat kaedah statik khas dan entah bagaimana menggunakan kaedah itu melalui invokedynamic.

Sepanjang artikel ini, kita akan melihat bagaimana permohonan ini berfungsi secara dalaman. Tetapi, pertama, mari kita tentukan masalah yang cuba diselesaikan oleh invokedynamic .

2.3. Masalah

Sebelum Java 7, JVM hanya mempunyai empat jenis kaedah invokasi: invokevirtual untuk memanggil kaedah kelas biasa, invokestatic untuk memanggil kaedah statik, invokeinterface untuk memanggil kaedah antara muka, dan invokespecial untuk memanggil konstruktor atau kaedah swasta.

Walaupun terdapat perbezaan, semua doa ini mempunyai satu sifat sederhana: Mereka mempunyai beberapa langkah yang telah ditentukan untuk menyelesaikan setiap panggilan kaedah, dan kami tidak dapat memperkaya langkah-langkah ini dengan tingkah laku tersuai kami.

Terdapat dua jalan penyelesaian utama untuk had ini: Satu pada masa kompilasi dan yang lain pada waktu proses. Yang pertama biasanya digunakan oleh bahasa seperti Scala atau Koltin dan yang terakhir adalah penyelesaian pilihan untuk bahasa dinamik berasaskan JVM seperti JRuby.

Pendekatan runtime biasanya berdasarkan refleksi dan akibatnya, tidak cekap.

Sebaliknya, penyelesaian masa kompilasi biasanya bergantung pada penjanaan kod pada waktu kompilasi. Pendekatan ini lebih cekap semasa menjalankan. Walau bagaimanapun, ia agak rapuh dan juga boleh menyebabkan masa permulaan yang lebih perlahan kerana terdapat lebih banyak kod byk untuk diproses.

Sekarang setelah kita mendapat pemahaman yang lebih baik mengenai masalah ini, mari kita lihat bagaimana penyelesaiannya berfungsi secara dalaman.

3. Di bawah Tudung

invokedynamic membolehkan kita memancing proses pemanggilan kaedah dengan cara yang kita mahukan . Iaitu, ketika JVM melihat opcodeynamic opcode untuk pertama kalinya, ia memanggil kaedah khas yang dikenali sebagai kaedah bootstrap untuk memulakan proses pemanggilan:

Kaedah bootstrap adalah sekeping kod Java yang biasa kami tulis untuk mengatur proses pemanggilan. Oleh itu, ia boleh mengandungi sebarang logik.

Setelah kaedah bootstrap selesai seperti biasa, ia harus mengembalikan contoh CallSite. Ini CallSite merangkumi keping maklumat berikut:

  • Penunjuk kepada logik sebenar yang harus dilaksanakan oleh JVM. Ini harus ditunjukkan sebagai MethodHandle.
  • Syarat yang mewakili kesahihan CallSite yang dikembalikan .

Mulai sekarang, setiap kali JVM melihat opcode ini lagi, ia akan melewati jalan perlahan dan secara langsung memanggil pelaksanaan yang mendasari . Lebih-lebih lagi, JVM akan terus melangkau jalan perlahan sehingga keadaan di CallSite berubah.

Berbanding dengan Reflection API, JVM dapat sepenuhnya Metode Handle yang dapat dilihat dan akan berusaha mengoptimumkannya, oleh itu prestasi yang lebih baik.

3.1. Jadual Kaedah Bootstrap

Mari kita lihat lagi bytecode invokedynamic yang dihasilkan :

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

Ini bermaksud bahawa arahan khusus ini harus memanggil kaedah bootstrap pertama (bahagian # 0) dari jadual kaedah bootstrap. Juga, ia menyebutkan beberapa argumen untuk meneruskan kaedah 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

Sebelum Java 9, penggabungan rentetan non-sepele dilaksanakan menggunakan StringBuilder. Sebagai sebahagian daripada JEP 280, penggabungan tali kini menggunakan invokedynamic. Sebagai contoh, mari kita gabungkan rentetan tetap dengan pemboleh ubah rawak:

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

Begini rupa bentuk bytecode untuk contoh ini:

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

Lebih-lebih lagi, kaedah bootstrap untuk penggabungan tali terdapat di kelas 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. Kesimpulan

Dalam artikel ini, pertama, kita membiasakan diri dengan masalah yang ingin diselesaikan oleh india.

Kemudian, dengan melihat contoh ungkapan lambda yang sederhana, kami melihat bagaimana invokedynamic berfungsi secara dalaman.

Akhirnya, kami menghitung beberapa contoh indy lain dalam versi Java baru-baru ini.