1. 概述
Invoke Dynamic(也称为Indy)是JSR 292的一部分,旨在增强JVM对动态类型语言的支持。在Java 7中首次发布后,invokedynamic操作码被JRuby等基于JVM的动态语言甚至Java等静态类型语言广泛使用。
在本教程中,我们将揭开invokedynamic的神秘面纱,看看它如何帮助库和语言设计者实现多种形式的动态性。
2. 认识Invoke Dynamic
让我们从一个简单的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();
}
}
起初,我们可能认为Java创建了一个派生自Predicate的匿名内部类,然后将该实例传递给filter方法。但是,我们错了。
2.1 字节码
为了验证这个假设,我们可以看一下生成的字节码:
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
尽管我们这么想,但实际上并不存在匿名内部类,当然,没有人将此类的实例传递给filter方法。
令人惊讶的是,invokedynamic指令以某种方式负责创建Predicate实例。
2.2 Lambda特定方法
此外,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
此方法将字符串作为输入,然后执行以下步骤:
- 计算输入长度(invokevirtual on length)
- 将长度与常量3(if_icmple和iconst_3)进行比较
- 如果长度小于或等于3,则返回false
有趣的是,这实际上等同于我们传递给filter方法的Lambda:
c -> c.length() > 3
因此,Java没有创建匿名内部类,而是创建了一个特殊的静态方法,并以某种方式通过invokedynamic调用该方法。
在本文中,我们将了解此调用的内部工作原理。但首先,让我们定义invokedynamic试图解决的问题。
2.3 问题
在Java 7之前,JVM只有4种方法调用类型:调用普通类方法的invokevirtual,调用静态方法的invokestatic,调用接口方法的invokeinterface,调用构造函数或私有方法的invokespecial。
尽管存在差异,但所有这些调用都有一个简单的特征:它们有几个预定义的步骤来完成每个方法调用,并且我们无法通过自定义行为来丰富这些步骤。
此限制有两种主要的解决方法:一种在编译时,另一种在运行时。前者通常由Scala或Koltin等语言使用,而后者是JRuby等基于JVM的动态语言的首选解决方案。
运行时方法通常是基于反射的,因此效率低下。
另一方面,编译时解决方案通常依赖于编译时的代码生成。这种方法在运行时更有效,但是,它有点脆弱,并且还可能导致启动时间变慢,因为要处理的字节码更多。
现在我们已经对问题有了更好的理解,让我们看看解决方案在内部是如何工作的。
3. 幕后
invokedynamic让我们以任何我们想要的方式引导方法调用过程。也就是说,当JVM第一次看到invokedynamic操作码时,它会调用一种称为bootstrap方法的特殊方法来初始化调用过程:
bootstrap方法是我们为设置调用过程而编写的一段普通Java代码。因此,它可以包含任何逻辑。
一旦bootstrap方法正常完成,它应该返回一个CallSite的实例,这个CallSite封装了以下信息:
- 指向JVM应执行的实际逻辑的指针,这应该表示为MethodHandle。
- 表示返回的CallSite有效性的条件。
从现在开始,每次JVM再次看到这个特定的操作码时,它都会跳过慢速路径,直接调用底层可执行文件。此外,JVM将继续跳过慢速路径,直到CallSite中的条件发生变化。
相当于反射API,JVM可以完全看穿MethodHandle并且会尝试对其进行优化,因此性能更好。
3.1 Bootstrap方法表
我们再看一下生成的invokedynamic字节码:
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
这意味着该特定指令应从bootstrap方法表中调用第一个bootstrap方法(#0部分)。此外,它还提到了一些要传递给bootstrap方法的参数:
- test是Predicate中唯一的抽象方法
- ()Ljava/util/function/Predicate表示JVM中的方法签名:该方法不接收任何输入并返回Predicate接口的实例
为了查看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
所有Lambda的bootstrap方法都是LambdaMetafactory类中的metafactory静态方法。
与所有其他bootstrap方法类似,这个方法至少需要3个参数,如下所示:
- Ljava/lang/invoke/MethodHandles$Lookup参数表示invokedynamic的查找上下文
- Ljava/lang/String表示调用站点中的方法名称-在这个例子中,方法名称是test
- Ljava/lang/invoke/MethodType是调用站点的动态方法签名-在本例中,它是()Ljava/util/function/Predicate
除了这3个参数之外,bootstrap方法还可以选择性地接收一个或多个额外参数。在这个例子中,这些是额外的:
- (Ljava/lang/Object;)Z是一个被擦除的方法签名,它接收一个Object实例并返回一个布尔值。
- REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z是指向实际Lambda逻辑的MethodHandle。
- (Ljava/lang/String;)Z是一个非擦除的方法签名,它接收一个字符串并返回一个布尔值。
简而言之,JVM会将所有需要的信息传递给bootstrap方法,bootstrap方法将依次使用该信息创建适当的Predicate实例。然后,JVM将该实例传递给filter方法。
3.2 不同类型的CallSite
一旦JVM第一次看到此示例中的invokedynamic,它就会调用bootstrap方法。在撰写本文时,Lambda bootstrap方法将使用InnerClassLambdaMetafactory在运行时为Lambda生成内部类。
然后bootstrap方法将生成的内部类封装在一种称为ConstantCallSite的特殊类型的CallSite中,这种类型的CallSite在设置后永远不会改变。因此,在为每个Lambda首次设置后,JVM将始终使用快速路径直接调用Lambda逻辑。
尽管这是最有效的invokedynamic类型,但它肯定不是唯一可用的选项。事实上,Java提供了MutableCallSite和VolatileCallSite以适应更动态的需求。
3.3 优点
因此,为了实现Lambda表达式,Java不是在编译时创建匿名内部类,而是在运行时通过invokedynamic创建它们。
有人可能会反对将内部类的生成推迟到运行时。但是,与简单的编译时解决方案相比,invokedynamic方法有一些优势。
首先,JVM在第一次使用Lambda之前不会生成内部类。因此,在第一次执行Lambda之前,我们无需为与内部类相关的额外占用空间付出代价。
此外,许多链接逻辑都从字节码移到了bootstrap方法中。因此,invokedynamic字节码通常比替代解决方案小得多。较小的字节码可以提高启动速度。
假设更新版本的Java带有更高效的bootstrap方法实现,那么我们的invokedynamic字节码就可以利用这一改进而无需重新编译,这样我们就可以实现某种转发二进制兼容性。基本上,我们可以在不同的策略之间切换而无需重新编译。
最后,用Java编写bootstrap和链接逻辑通常比遍历AST生成一段复杂的字节码更容易。因此,invokedynamic可以(主观上)不那么脆弱。
4. 更多示例
Lambda表达式并不是唯一的特性,Java当然也不是唯一使用invokedynamic的语言。在本节中,我们将熟悉一些InvokeDynamic的其他示例。
4.1 Java 14:记录
记录是Java 14中的一项新预览功能,它提供了一种简洁的语法来声明应该是哑数据持有者的类。
这是一个简单的记录示例:
public record Color(String name, int code) {}
给定这个简单的单行代码,Java编译器会为访问器方法、toString、equals和hashcode生成适当的实现。
为了实现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
另一种解决方案是找到所有记录字段,并在编译时根据这些字段生成equals逻辑。字段越多,字节码就越长。
相反,Java在运行时调用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:字符串拼接
在Java 9之前,重要的字符串拼接是使用StringBuilder实现的。作为JEP 280的一部分,字符串拼接现在使用invokedynamic。 例如,让我们拼接一个常量字符串和一个随机变量:
"random-" + ThreadLocalRandom.current().nextInt();
以下是此示例的字节码:
0: invokestatic #7 // Method ThreadLocalRandom.current:()LThreadLocalRandom;
3: invokevirtual #13 // Method ThreadLocalRandom.nextInt:()I
6: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)LString;
此外,字符串拼接的bootstrap方法位于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. 总结
在本文中,我们首先熟悉了Indy试图解决的问题。
然后,通过一个简单的Lambda表达式示例,我们了解了invokedynamic的内部工作原理。
最后,我们列举了最近几个Java版本中indy的其他几个例子。
Post Directory
