方法分派(dispatch)
从Java 1.0到现在,invokedynamic
是第一个新加入的Java字节码,它与已有的字节码invokevirtual
、invokestatic
、invokeinterface
和invokespecial
组合在了一起。已有的这四个操作码实现了Java开发人员所熟知的所有形式的方法分派(dispatch):
- invokevirtual——对实例方法的标准分派
- invokestatic——用于分派静态方法
- invokeinterface——用于通过接口进行方法调用的分派
- invokespecial——当需要进行非虚(也就是“精确”)分派时会用到
invokedynamic
invokedynamic
是做什么的,它对于Java开发人员有什么用处呢?
Java的调用函数的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符号引用在静态类型语言编译时就能产生,而动态类型语言只有在运行期才能确定接收者类型,改变四大指令的语意对java的版本有很大的影响,所以在JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一个新的指令invokedynamic
.
这个特性的主要目标在于创建一个字节码,用于处理新型的方法分派——它的本质是允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断(也就是动态绑定)。这样的话,相对于Java平台之前所提供的编程风格,允许语言和框架的编写人员支持更加动态的编码风格。
它的目的在于由用户代码通过方法句柄API(method handles API)在运行时确定如何分派,同时避免反射带来的性能惩罚
和安全
问题。实际上,invokedynamic所宣称的目标就是: 它的速度要像常规的方法分派(invokevirtual)一样快。
当Java 7发布的时候,JVM就已经支持执行新的字节码了,但是不管提交什么样的Java代码,javac都不会产生包含invokedynamic的字节码。这项特性用来支持JRuby,Groovy等其他运行在JVM上的动态语言。
在Java 8中,这发生了变化,在实现lambda
表达式和默认方
法时,底层会生成和使用invokedynamic,它同时还会作为Nashorn的首选分派机制。但是,对于Java应用的开发人员来说,依然没有直接的方式实现完全的动态方法处理(resolution)。也就是说,Java语言并没有提供关键字或库来创建通用的invokedynamic调用点(call site)。这意味着,尽管这种机制的功能非常强大,但它对于大多数的Java开发人员来说依然有些陌生。接下来,我们看一下如何在自己的代码中使用这项技术。
import java.util.function.Consumer;
//新的lambda的方式,会产生invokedynamic调用
public class LambdaNew {
public static void main(String[] args) {
Consumer<String> c = s -> System.out.println(s);
c.accept("hello lambda!");
}
}
其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加上
-Djdk.internal.lambda.dumpProxyClasse
s ,加上这个参数后,运行时会将生成的内部类class码输出到一个文件中。
首先自己觉得java的处理方式应该是按照内部匿名类的方式来处理
import java.util.function.Consumer;
//传统的内部匿名类的方式,不会产生invokedynamic调用
public class LambdaOld {
public static void main(String[] args) {
Consumer<String> c = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
c.accept("hello lambda");
}
}
猜测在支持动态语言是java是换汤不换药,在最后编译的时候生成我们常见的方式;心想针对LambdaNew
类,编译的结果应该是LambdaNew.class
和 LambdaNew$1.class
这两个类; 但是结果不是这样的,只是产生了一个LambdaNew.class
这一个类
反编译吧,来看看真相是什么?
执行: javap -v -p LambdaNew.class
Classfile /E:/WJW_DATA/OpenSource/SpringBoot-1/eclipse_workspace/Java8/bin/wjw/test/java8/invokedynamic/LambdaNew.class
Last modified 2018-3-6; size 1614 bytes
MD5 checksum 3ff1f95a4ce171bdc2b7a056e5ef5539
Compiled from "LambdaNew.java"
public class wjw.test.java8.invokedynamic.LambdaNew
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // wjw/test/java8/invokedynamic/LambdaNew
#2 = Utf8 wjw/test/java8/invokedynamic/LambdaNew
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <clinit>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = String #9 // jdk.internal.lambda.dumpProxyClasses
#9 = Utf8 jdk.internal.lambda.dumpProxyClasses
#10 = String #11 // .
#11 = Utf8 .
#12 = Methodref #13.#15 // java/lang/System.setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#13 = Class #14 // java/lang/System
#14 = Utf8 java/lang/System
#15 = NameAndType #16:#17 // setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#16 = Utf8 setProperty
#17 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 <init>
#21 = Methodref #3.#22 // java/lang/Object."<init>":()V
#22 = NameAndType #20:#6 // "<init>":()V
#23 = Utf8 this
#24 = Utf8 Lwjw/test/java8/invokedynamic/LambdaNew;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = NameAndType #28:#29 // accept:()Ljava/util/function/Consumer;
#28 = Utf8 accept
#29 = Utf8 ()Ljava/util/function/Consumer;
#30 = InvokeDynamic #0:#27 // #0:accept:()Ljava/util/function/Consumer;
#31 = String #32 // hello lambda!
#32 = Utf8 hello lambda!
#33 = InterfaceMethodref #34.#36 // java/util/function/Consumer.accept:(Ljava/lang/Object;)V
#34 = Class #35 // java/util/function/Consumer
#35 = Utf8 java/util/function/Consumer
#36 = NameAndType #28:#37 // accept:(Ljava/lang/Object;)V
#37 = Utf8 (Ljava/lang/Object;)V
#38 = Utf8 args
#39 = Utf8 [Ljava/lang/String;
#40 = Utf8 c
#41 = Utf8 Ljava/util/function/Consumer;
#42 = Utf8 LocalVariableTypeTable
#43 = Utf8 Ljava/util/function/Consumer<Ljava/lang/String;>;
#44 = Utf8 lambda$0
#45 = Utf8 (Ljava/lang/String;)V
#46 = Fieldref #13.#47 // java/lang/System.out:Ljava/io/PrintStream;
#47 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Methodref #51.#53 // java/io/PrintStream.println:(Ljava/lang/String;)V
#51 = Class #52 // java/io/PrintStream
#52 = Utf8 java/io/PrintStream
#53 = NameAndType #54:#45 // println:(Ljava/lang/String;)V
#54 = Utf8 println
#55 = Utf8 s
#56 = Utf8 Ljava/lang/String;
#57 = Utf8 SourceFile
#58 = Utf8 LambdaNew.java
#59 = Utf8 BootstrapMethods
#60 = Methodref #61.#63 // 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;
#61 = Class #62 // java/lang/invoke/LambdaMetafactory
#62 = Utf8 java/lang/invoke/LambdaMetafactory
#63 = NameAndType #64:#65 // 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;
#64 = Utf8 metafactory
#65 = Utf8 (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;
#66 = MethodHandle #6:#60 // 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;
#67 = MethodType #37 // (Ljava/lang/Object;)V
#68 = Methodref #1.#69 // wjw/test/java8/invokedynamic/LambdaNew.lambda$0:(Ljava/lang/String;)V
#69 = NameAndType #44:#45 // lambda$0:(Ljava/lang/String;)V
#70 = MethodHandle #6:#68 // invokestatic wjw/test/java8/invokedynamic/LambdaNew.lambda$0:(Ljava/lang/String;)V
#71 = MethodType #45 // (Ljava/lang/String;)V
#72 = Utf8 InnerClasses
#73 = Class #74 // java/lang/invoke/MethodHandles$Lookup
#74 = Utf8 java/lang/invoke/MethodHandles$Lookup
#75 = Class #76 // java/lang/invoke/MethodHandles
#76 = Utf8 java/lang/invoke/MethodHandles
#77 = Utf8 Lookup
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc #8 // String jdk.internal.lambda.dumpProxyClasses
2: ldc #10 // String .
4: invokestatic #12 // Method java/lang/System.setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
7: pop
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
public wjw.test.java8.invokedynamic.LambdaNew();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #21 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lwjw/test/java8/invokedynamic/LambdaNew;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: invokedynamic #30, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
5: astore_1
6: aload_1
7: ldc #31 // String hello lambda!
9: invokeinterface #33, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
14: return
LineNumberTable:
line 11: 0
line 12: 6
line 13: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 args [Ljava/lang/String;
6 9 1 c Ljava/util/function/Consumer;
LocalVariableTypeTable:
Start Length Slot Name Signature
6 9 1 c Ljava/util/function/Consumer<Ljava/lang/String;>;
private static void lambda$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #46 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 s Ljava/lang/String;
}
SourceFile: "LambdaNew.java"
BootstrapMethods:
0: #66 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:
#67 (Ljava/lang/Object;)V
#70 invokestatic wjw/test/java8/invokedynamic/LambdaNew.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
InnerClasses:
public static final #77= #73 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
在这里我们发现了几个与我们常见的java不太一样的地方invokedynamic
- invokedynamic
0: invokedynamic #30, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
#30 是代表常量#30 也就是后面的注释InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
0 是占位符号,目前无用
- BootstrapMethods(BSM)
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法. 引导方法是由编译器生成, 在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为.引导方法的返回值类型是java.lang.invoke.CallSite
, 一个invokedynamic指令关联一个CallSite, 将所有的调用委托到CallSite当前的target(MethodHandle)
// InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
就是BootstrapMethods表示#0的位置
BootstrapMethods:
0: #66 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:
#67 (Ljava/lang/Object;)V
#70 invokestatic wjw/test/java8/invokedynamic/LambdaNew.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
我们看到调用了LambdaMetaFactory.metafactory
的方法
参数:
*LambdaMetafactory.metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType)*有六个参数, 按顺序描述如下
- MethodHandles.Lookup
caller
: 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动填充这个参数 - String
invokedName
: 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们自动填充为 “accept”, 即Consumer.accept方法名. - MethodType
invokedType
: 调用点期望的方法参数的类型和返回值的类型(方法signature). 使用invokedynamic指令时, JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type), 在这里参数为String, 返回值类型为Consumer, 表示这个调用点的目标方法的参数为String, 然后invokedynamic执行完后会返回一个即Consumer实例. - MethodType
samMethodType
: 函数对象将要实现的接口方法类型, 这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除).#67 (Ljava/lang/Object;)V
- MethodHandle
implMethod
: 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数), 在这里为#70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
方法的方法句柄. - MethodType
instantiatedMethodType
: 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型:
比如函数接口方法定义为 void accept(T t)
T为泛型标识, 这个时候方法类型为(Object)Void
, 在编译时T已确定, 即T由String替换, 这时samMethodType就是 (Object)Void
, 而instantiatedMethodType为(String)Void
.
前3个参数JVM会自动填充;第4, 5, 6 三个参数来自class文件中. 如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数.
Method arguments:
#67 (Ljava/lang/Object;)V
#70 invokestatic wjw/test/java8/invokedynamic/LambdaNew.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
我们来看metafactory 的方法里的实现代码
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
在buildCallSite的函数中
CallSite buildCallSite() throws LambdaConversionException {
final Class<?> innerClass = spinInnerClass();
...
}
函数spinInnerClass
构建了这个内部类,也就是生成了一个LambdaNew$$Lambda$1 这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数 System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
会在你指定的路径上生成这个内部类:
package wjw.test.java8.invokedynamic;
import java.util.function.*;
import java.lang.invoke.*;
final class LambdaNew$$Lambda$1 implements Consumer
{
@LambdaForm.Hidden
@Override
public void accept(final Object o) {
LambdaNew.lambda$0((String)o);
}
}
这个内部类实现了函数式接口Consumer
,并且在accept
中调用了LambdaNew.lambda$0
静态函数,也就是表达式中的函数块.
方法句柄(MethodHandle)
要让invokedynamic正常运行,一个核心的概念就是方法句柄(method handle)。它代表了一个可以从invokedynamic调用点进行调用的方法。这里的基本理念就是每个invokedynamic指令都会与一个特定的方法关联(也就是引导方法)。当解释器(interpreter)遇到invokedynamic指令的时候,BSM会被调用。它会返回一个对象(包含了一个方法句柄),这个对象表明了调用点要实际执行哪个方法。
invokedynamic指令通过引导方法(bootstrap method,BSM)机制来使用方法句柄。与invokevirtual指令不同,invokedynamic指令没有接收者对象。相反,它们的行为类似于invokestatic,会使用BSM来返回一个CallSite类型的对象。这个对象包含一个方法句柄(称之为“target”),它代表了当前invokedynamic指令要执行的方法。
在一定程度上,这与反射有些类似,但是反射有它的局限性,这些局限性使它不适合与invokedynamic协作使用。Java 7 API中加入了java.lang.invoke.MethodHandle
(及其子类),通过它们来代表invokedynamic指向的方法。为了实现操作的正确性,MethodHandle会得到JVM的一些特殊处理。
理解方法句柄的一种方式就是将其视为以安全、现代的方式来实现反射的核心功能,在这个过程会尽可能地保证类型的安全。invokedynamic需要方法句柄,另外它们也可以单独使用。
方法类型(MethodType)
一个Java方法可以视为由四个基本内容所构成:
- 名称
- 签名(包含返回类型)
- 定义它的类
- 实现方法的字节码
这意味着如果要引用某个方法,我们需要有一种有效的方式来表示方法签名(而不是反射中强制使用的令人讨厌的Class<?>[] hack方式)。
接下来我们采用另外的方式,方法句柄首先需要的一个构建块就是表达方法签名的方式,以便于查找。在Java 7引入的Method Handles API中,这个角色是由java.lang.invoke.MethodType
类来完成的,它使用一个不可变的实例来代表签名。要获取MethodType,我们可以使用methodType()
工厂方法。这是一个参数可变的方法,以class对象作为参数。
第一个参数所使用的class对象,对应着签名的返回类型;剩余参数中所使用的class对象,对应着签名中方法参数的类型。例如:
//返回类型是String的签名,例如:toString(),toUpperCase()
MethodType mtToString = MethodType.methodType(String.class);
//返回类型是void,参数类型是Object的签名,例如: setter方法
MethodType mtSetter = MethodType.methodType(void.class, Object.class);
//返回类型是int,参数类型是String,String的签名,例如: Comparator中compare()方法
MethodType mtStringComparator = MethodType.methodType(int.class, String.class, String.class);
现在我们就可以使用MethodType,再组合方法名称以及定义方法的类来查找方法句柄。
整理
现在, 我们知道了:
- Lambda表达的内容被编译成了当前类的一个静态方法.
- Lambda表达式所在处会产生一条invokedynamic指令调用, 同时编译器会生成一个对应的Bootstrap Method.
- 当JVM第一次碰到这条invokedynamic时, 会调用对应的Bootstrap方法.
- 由Lambda表达式产生的invokedynamic指令的引导方法是调用LambdaMetafactory.metafactory()方法.
- 调用引导方法会返回一个CallSite对象实例, 该实例target引用一个MethodHandle实例.
- 执行MethodHanlde代表的方法, 返回结果, 结果为动态生成的接口实例, 接口实现调用
1.
中生成的实例或者静态方法(取决于Lambda表达式出现在实例方法中还是静态方法中).