Pengantar Memanggil Dinamis di JVM

1. Ikhtisar

Invoke Dynamic (Juga dikenal sebagai Indy) adalah bagian dari JSR 292 yang dimaksudkan untuk meningkatkan dukungan JVM untuk bahasa yang diketik secara dinamis. Setelah rilis pertama di Java 7, opcode dinamis yang dipanggil digunakan cukup luas oleh bahasa berbasis JVM dinamis seperti JRuby dan bahkan bahasa yang diketik secara statis seperti Java.

Dalam tutorial ini, kita akan mengungkap invokedynamic dan melihat bagaimana hal itu bisamembantu perancang perpustakaan dan bahasa untuk mengimplementasikan berbagai bentuk dinamika.

2. Temui Invoke Dynamic

Mari kita mulai dengan rangkaian panggilan API Stream sederhana:

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

Pada awalnya, kita mungkin berpikir bahwa Java membuat kelas dalam anonim yang berasal dari Predicate dan kemudian meneruskan instance itu ke metode filter . Tapi, kami salah.

2.1. The Bytecode

Untuk memeriksa asumsi ini, kita dapat mengintip bytecode 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

Terlepas dari apa yang kami pikirkan, tidak ada kelas dalam anonim dan tentu saja, tidak ada yang meneruskan instance kelas seperti itu ke metode filter .

Anehnya, instruksi dinamik yang dipanggil entah bagaimana bertanggung jawab untuk membuat instance Predicate .

2.2. Metode Khusus Lambda

Selain itu, compiler Java juga membuat metode statis yang tampak lucu berikut ini:

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

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

  • Menghitung panjang input (invokevirtual on length )
  • Membandingkan panjang dengan konstanta 3 ( if_icmple dan iconst_3 )
  • Menampilkan salah jika panjangnya kurang dari atau sama dengan 3

Menariknya, ini sebenarnya setara dengan lambda yang kami teruskan ke metode filter :

c -> c.length() > 3

Jadi, alih-alih kelas dalam anonim, Java membuat metode statis khusus dan entah bagaimana memanggil metode itu melalui invokedynamic.

Selama artikel ini, kita akan melihat bagaimana doa ini bekerja secara internal. Tapi, pertama, mari kita definisikan masalah yang coba dipecahkan oleh invokedynamic .

2.3. Masalah

Sebelum Java 7, JVM hanya memiliki empat jenis metode doa: invokevirtual untuk memanggil metode kelas normal, invokestatic untuk memanggil metode statis, invokeinterface metode antarmuka panggilan, dan invokespecial panggilan konstruktor atau metode pribadi.

Terlepas dari perbedaannya, semua pemanggilan ini memiliki satu ciri sederhana: Mereka memiliki beberapa langkah yang telah ditentukan untuk menyelesaikan setiap pemanggilan metode, dan kami tidak dapat memperkaya langkah-langkah ini dengan perilaku khusus kami.

Ada dua solusi utama untuk batasan ini: Satu pada waktu kompilasi dan yang lainnya pada waktu proses. Yang pertama biasanya digunakan oleh bahasa seperti Scala atau Koltin dan yang terakhir adalah solusi pilihan untuk bahasa dinamis berbasis JVM seperti JRuby.

Pendekatan runtime biasanya berbasis refleksi dan akibatnya, tidak efisien.

Di sisi lain, solusi waktu kompilasi biasanya mengandalkan pembuatan kode pada waktu kompilasi. Pendekatan ini lebih efisien pada waktu proses. Namun, ini agak rapuh dan juga dapat menyebabkan waktu startup lebih lambat karena ada lebih banyak bytecode untuk diproses.

Sekarang setelah kita memiliki pemahaman yang lebih baik tentang masalahnya, mari kita lihat bagaimana solusinya bekerja secara internal.

3. Di Balik Terpal

invokedynamic memungkinkan kita mem- bootstrap proses pemanggilan metode dengan cara apa pun yang kita inginkan . Artinya, saat JVM melihatopcode dinamis yang dipanggil untuk pertama kalinya,JVMakan memanggil metode khusus yang dikenal sebagai metode bootstrap untuk menginisialisasi proses pemanggilan:

Metode bootstrap adalah bagian normal dari kode Java yang telah kami tulis untuk menyiapkan proses pemanggilan. Oleh karena itu, dapat mengandung logika apa pun.

Setelah metode bootstrap selesai secara normal, ia harus mengembalikan instance CallSite. CallSite ini merangkum informasi berikut:

  • Penunjuk ke logika sebenarnya yang harus dijalankan JVM. Ini harus direpresentasikan sebagai MethodHandle.
  • Kondisi yang mewakili validitas CallSite yang dikembalikan .

Mulai sekarang, setiap kali JVM melihat opcode khusus ini lagi, itu akan melewati jalur lambat dan langsung memanggil executable yang mendasarinya . Selain itu, JVM akan terus melewati jalur lambat hingga kondisi di CallSite berubah.

Berbeda dengan Reflection API, JVM dapat sepenuhnya melihat-lihat MethodHandle dan akan mencoba mengoptimalkannya, sehingga kinerjanya lebih baik.

3.1. Tabel Metode Bootstrap

Mari kita lihat lagi bytecode dipanggil yang dihasilkan :

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

Ini berarti bahwa instruksi khusus ini harus memanggil metode bootstrap pertama (bagian # 0) dari tabel metode bootstrap. Juga, ini menyebutkan beberapa argumen untuk diteruskan ke metode 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 string non-sepele diimplementasikan menggunakan StringBuilder. Sebagai bagian dari JEP 280, penggabungan string sekarang menggunakan invokedynamic. Misalnya, mari kita gabungkan string konstan dengan variabel acak:

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

Begini tampilan 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;

Selain itu, metode bootstrap untuk penggabungan string berada 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

Pada artikel ini, pertama, kita mengenal masalah yang coba diselesaikan oleh indy.

Kemudian, dengan menelusuri contoh ekspresi lambda sederhana, kami melihat bagaimana invokedynamic bekerja secara internal.

Terakhir, kami menyebutkan beberapa contoh indy lainnya di Java versi terbaru.