1. Overview
1.概述
Invoke Dynamic (Also known as Indy) was part of JSR 292 intended to enhance the JVM support for dynamically typed languages. After its first release in Java 7, the invokedynamic opcode is used quite extensively by dynamic JVM-based languages like JRuby and even statically typed languages like Java.
Invoke Dynamic(也被称为Indy)是JSR 292的一部分,旨在增强JVM对动态类型语言的支持。在Java 7中首次发布后, invokedynamic操作码被JRuby等基于JVM的动态语言甚至Java等静态类型语言相当广泛地使用。
In this tutorial, we’re going to demystify invokedynamic and see how it can help library and language designers to implement many forms of dynamicity.
在本教程中,我们将揭开invokedynamic的神秘面纱,看看它如何帮助库和语言设计师实现多种形式的动态性。
2. Meet Invoke Dynamic
2.满足调用动态
Let’s start with a simple chain of Stream API calls:
让我们从一个简单的Stream API调用链开始。
public class Main {
public static void main(String[] args) {
long lengthyColors = List.of("Red", "Green", "Blue")
.stream().filter(c -> c.length() > 3).count();
}
}
At first, we might think that Java creates an anonymous inner class deriving from Predicate and then passes that instance to the filter method. But, we’d be wrong.
起初,我们可能会认为Java创建了一个源自Predicate的匿名内类,然后将该实例传递给filter方法。但是,我们就错了。
2.1. The Bytecode
2.1.比特码
To check this assumption, we can take a peek at the generated bytecode:
为了检查这个假设,我们可以偷看一下生成的字节码。
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
Despite what we thought, there’s no anonymous inner class and certainly, nobody is passing an instance of such a class to the filter method.
尽管我们认为,没有匿名的内部类,当然,也没有人把这样一个类的实例传递给filter method。
Surprisingly, the invokedynamic instruction is somehow responsible for creating the Predicate instance.
令人惊讶的是,invokedynamic指令以某种方式负责创建Predicate实例。
2.2. Lambda Specific Methods
2.2.兰姆达的具体方法
Additionally, the Java compiler also generated the following funny-looking static method:
此外,Java编译器还生成了下面这个看起来很滑稽的静态方法。
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
This method takes a String as the input and then performs the following steps:
这个方法接受一个字符串作为输入,然后执行以下步骤。
- Computing the input length (invokevirtual on length)
- Comparing the length with the constant 3 (if_icmple and iconst_3)
- Returning false if the length is less than or equal to 3
Interestingly, this is actually equivalent of the lambda we passed to the filter method:
有趣的是,这实际上等同于我们传递给filter方法的lambda。
c -> c.length() > 3
So instead of an anonymous inner class, Java creates a special static method and somehow invokes that method via invokedynamic.
因此,Java创建了一个特殊的静态方法,并以某种方式通过invokedynamic调用该方法,而不是一个匿名的内部类。
Over the course of this article, we’re going to see how this invocation works internally. But, first, let’s define the problem that invokedynamic is trying to solve.
在这篇文章中,我们将看到这个调用在内部是如何工作的。但是,首先,让我们定义一下invokedynamic所要解决的问题。
2.3. The Problem
2.3.该问题
Before Java 7, the JVM only had four method invocation types: invokevirtual to call normal class methods, invokestatic to call static methods, invokeinterface to call interface methods, and invokespecial to call constructors or private methods.
在Java 7之前,JVM只有四种方法调用类型。invokevirtual 调用正常的类方法,invokestatic 调用静态方法,invokeinterface 调用接口方法,以及invokespecial 调用构造函数或私有方法。
Despite their differences, all these invocations share one simple trait: They have a few predefined steps to complete each method call, and we can’t enrich these steps with our custom behaviors.
尽管有差异,但所有这些调用都有一个简单的特征。它们有几个预定义的步骤来完成每个方法的调用,而且我们不能用我们的自定义行为来丰富这些步骤。
There are two main workarounds for this limitation: One at compile-time and the other at runtime. The former is usually used by languages like Scala or Koltin and the latter is the solution of choice for JVM-based dynamic languages like JRuby.
对于这个限制,有两个主要的解决方法。一个是在编译时,另一个是在运行时。前者通常被Scala或Koltin等语言所采用,后者则是基于JVM的动态语言(如JRuby)的首选解决方案。
The runtime approach is usually reflection-based and consequently, inefficient.
运行时的方法通常是基于反射的,因此,效率很低。
On the other hand, the compile-time solution is usually relying on code-generation at compile-time. This approach is more efficient at runtime. However, it’s somewhat brittle and also may cause a slower startup time as there’s more bytecode to process.
另一方面,编译时的解决方案通常是在编译时依靠代码生成。这种方法在运行时更有效率。然而,它有点脆,也可能导致启动时间变慢,因为有更多的字节码需要处理。
Now that we’ve got a better understanding of the problem, let’s see how the solution works internally.
现在我们已经对这个问题有了更好的了解,让我们看看这个解决方案在内部是如何运作的。
3. Under the Hood
3.引擎盖下
invokedynamic lets us bootstrap the method invocation process in any way we want. That is, when the JVM sees an invokedynamic opcode for the first time, it calls a special method known as the bootstrap method to initialize the invocation process:
invokedynamic 让我们以任何我们想要的方式引导方法的调用过程。也就是说,当JVM第一次看到一个invokedynamic opcode时,它会调用一个被称为bootstrap方法的特殊方法来初始化调用过程。
The bootstrap method is a normal piece of Java code that we’ve written to set up the invocation process. Therefore, it can contain any logic.
bootstrap方法是一段普通的Java代码,我们编写它是为了设置调用过程。因此,它可以包含任何逻辑。
Once the bootstrap method completes normally, it should return an instance of CallSite. This CallSite encapsulates the following pieces of information:
一旦引导方法正常完成,它应该返回一个CallSite的实例。这个CallSite封装了以下信息。
- A pointer to the actual logic that JVM should execute. This should be represented as a MethodHandle.
- A condition representing the validity of the returned CallSite.
From now on, every time JVM sees this particular opcode again, it will skip the slow path and directly calls the underlying executable. Moreover, the JVM will continue to skip the slow path until the condition in the CallSite changes.
从现在开始,每当JVM再次看到这个特定的操作码时,它将跳过慢速路径,直接调用底层可执行程序。此外,JVM将继续跳过慢速路径,直到CallSite中的条件发生变化。
As opposed to the Reflection API, the JVM can completely see-through MethodHandles and will try to optimize them, hence the better performance.
与Reflection API相反,JVM可以完全看透MethodHandles,并会尝试优化它们,因此性能更好。
3.1. Bootstrap Method Table
3.1.Bootstrap方法表
Let’s take another look at the generated invokedynamic bytecode:
让我们再看一下生成的invokedynamicbytecode。
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
This means that this particular instruction should call the first bootstrap method (#0 part) from the bootstrap method table. Also, it mentions some of the arguments to pass to the bootstrap method:
这意味着这条特殊指令应该调用bootstrap方法表中的第一个bootstrap方法(#0部分)。此外,它还提到了一些要传递给引导方法的参数。
- 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:
为了看到lambda例子的bootstrap方法表,我们应该把-v选项传给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.
所有lambdas的引导方法是metafactory类中的LambdaMetafactory静态方法。
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:
除了这三个参数,bootstrap方法还可以选择接受一个或多个额外的参数。在这个例子中,这些是额外的参数。
- 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.
简单地说,JVM将把所有需要的信息传递给Bootstrap方法。Bootstrap方法将反过来使用这些信息来创建一个适当的Predicate实例。然后,JVM将把该实例传递给filtermethod。
3.2. Different Types of CallSites
3.2.不同类型的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 InnerClassLambdaMetafactory to generate an inner class for the lambda at runtime.
一旦JVM在这个例子中第一次看到invokedynamic,它就会调用bootstrap方法。在撰写本文时,lambda bootstrap方法将使用InnerClassLambdaMetafactory在运行时为lambda生成一个内部类。
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.
然后,bootstrap方法将生成的内部类封装在一个特殊类型的CallSite中,称为ConstantCallSite。这种类型的CallSite在设置后将永远不会改变。因此,在对每个lambda进行第一次设置后,JVM将始终使用快速路径来直接调用lambda逻辑。
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.
尽管这是最有效的invokedynamic类型,它当然不是唯一可用的选择。事实上,Java提供了MutableCallSite和VolatileCallSite以适应更多的动态需求。
3.3. Advantages
3.3.优势
So, in order to implement lambda expressions, instead of creating anonymous inner classes at compile-time, Java creates them at runtime via invokedynamic.
因此,为了实现lambda表达式,Java不是在编译时创建匿名内类,而是在运行时通过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.
人们可能会反对将内部类的生成推迟到运行时。然而,与简单的编译时解决方案相比,invokedynamic方法有一些优势。
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.
首先,JVM在第一次使用lambda之前不会生成内类。因此,在第一次执行lambda之前,我们不会为与内层类相关的额外足迹付费。
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.
此外,大部分的链接逻辑被从字节码中移出到引导方法中。因此,invokedynamicbytecode通常比其他解决方案小得多。较小的字节码可以提高启动速度。
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.
假设新版本的 Java 有一个更有效的引导方法实现。那么我们的invokedynamicbytecode就可以利用这一改进,而无需重新编译。这样我们就可以实现某种转发的二进制兼容。基本上,我们可以在不同的策略之间切换,而无需重新编译。
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.
最后,用 Java 编写引导和链接逻辑通常比遍历 AST 来生成复杂的字节码要容易。因此,invokedynamic可以(主观地)减少脆性。
4. More Examples
4.更多例子
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.
Lambda表达式并不是唯一的功能,Java也不一定是唯一使用invokedynamic的语言。在本节中,我们将熟悉其他几个动态调用的例子。
4.1. Java 14: Records
4.1 Java 14: 记录
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.
记录是Java 14中的一个新的预览功能,它提供了一个很好的简洁语法来声明那些应该是哑巴数据持有者的类。
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.
鉴于这个简单的单行代码,Java编译器为访问器方法、toString、equals、和hashcode生成了适当的实现。
In order to implement toString, equals, or hashcode, Java is using invokedynamic. For instance, the bytecode for equals is as follows:
为了实现toString、equals、或hashcode,Java正在使用invokedynamic。例如,equals的字节码如下。
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.
另一种解决方案是找到所有记录字段,并在编译时基于这些字段生成equals 逻辑。我们的字段越多,字节码就越长。
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.
相反,Java在运行时调用一个引导方法来链接适当的实现。因此,无论字段的数量如何,字节码的长度将保持不变。
Looking more closely at the bytecode shows that the bootstrap method is ObjectMethods#bootstrap:
更仔细地观察字节码可以发现,bootstrap方法是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
4.2.Java 9: 字符串连接
Prior to Java 9, non-trivial string concatenations were implemented using StringBuilder. As part of JEP 280, string concatenation is now using invokedynamic. For instance, let’s concatenate a constant string with a random variable:
在Java 9之前,非复杂的字符串连接是使用StringBuilder实现的。作为JEP 280的一部分,现在字符串连接是使用invokedynamic。例如,让我们将一个常数字符串与一个随机变量连接起来。
"random-" + ThreadLocalRandom.current().nextInt();
Here’s how the bytecode looks like for this example:
下面是这个例子的字节码的样子。
0: invokestatic #7 // Method ThreadLocalRandom.current:()LThreadLocalRandom;
3: invokevirtual #13 // Method ThreadLocalRandom.nextInt:()I
6: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)LString;
Moreover, the bootstrap methods for string concatenations are residing in the StringConcatFactory class:
此外,用于字符串连接的引导方法驻留在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. Conclusion
5.总结
In this article, first, we got familiar with the problems the indy is trying to solve.
在这篇文章中,首先,我们熟悉了印度人所要解决的问题。
Then, by walking through a simple lambda expression example, we saw how invokedynamic works internally.
然后,通过走过一个简单的lambda表达式例子,我们看到了invokedynamic的内部运作。
Finally, we enumerated a few other examples of indy in recent versions of Java.
最后,我们列举了最近版本的Java中其他几个indy的例子。