1.前言
2014年十月份的时候Debug了下Lambda的实现代码, 大概了解了Lambda的实现, 昨天回忆了下, 发现以忘光, 还是写篇博客吧, 方便记忆
这篇文章是我本地Debug后记录下来的所见所闻, 不一定完全正确, 如有错误, 请务必指出.
2.环境
JDK: Oracle JDK1.8.0_05 64位 , Eclipse4.4
3.过程
初看Lambda时以为Lambda就是编译器帮我们把Lambda表达式给编译成了一个匿名内部类, 然后调用, 但是偶然间看到字节码文件后, 发现有点不同, 于是研究了下.
//源码是这样的
public static void main(String[] args) throws Throwable {
String hello = "hello lambda ";
Function<String, Void> func = (name) -> {
System.out.println(hello + name);
return null;
};
func.apply("haogrgr");
}
//原本以为编译器会将Lambda表达式编译成这样.
String hello = "hello lambda ";
Function<String, Void> func = new Function<String, Void>() {
@Override public Void apply(String name) {
System.out.println(hello + name);
return null;
}
}
func.apply("haogrgr");
//但是发现字节码是这样的
//main方法块
public static void main(java.lang.String[] args) throws java.lang.Throwable;
0 ldc <String "hello lambda "> [19]
2 astore_1 [hello]
3 aload_1 [hello]
4 invokedynamic 0 apply(java.lang.String) : java.util.function.Function [24]
9 astore_2 [func]
10 aload_2 [func]
11 ldc <String "haogrgr"> [25]
13 invokeinterface java.util.function.Function.apply(java.lang.Object) : java.lang.Object [27] [nargs: 2]
18 pop
19 return
//Lambda表达式的内容被编译器编译成了当前类的一个static方法 命名为lambda$0, 将使用到的外部变量用参数替代.
//synthetic 标记表示这个方法是否由编译器产生.(字段或方法访问标志ACC_SYNTHETIC)
private static synthetic java.lang.Void lambda$0(java.lang.String arg0, java.lang.String name);
0 getstatic java.lang.System.out : java.io.PrintStream [42]
3 new java.lang.StringBuilder [48]
6 dup
7 aload_0 [arg0]
8 invokestatic java.lang.String.valueOf(java.lang.Object) : java.lang.String [50]
11 invokespecial java.lang.StringBuilder(java.lang.String) [56]
14 aload_1 [name]
15 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [59]
18 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [63]
21 invokevirtual java.io.PrintStream.println(java.lang.String) : void [67]
24 aconst_null
25 areturn
//这里是invokedynamic指令的引导方法
Bootstrap methods:
0 : # 82 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:
#83 (Ljava/lang/Object;)Ljava/lang/Object;
#86 invokestatic com/haogrgr/java8/main/Main.lambda$0:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Void;
#88 (Ljava/lang/String;)Ljava/lang/Void;
先看看invokedynamic 0 apply(java.lang.String) : java.util.function.Function [24] 这个指令 (介绍可以参考周志明的 <深入理解Java虚拟机 第二版> 263页)
这个指令形式为 invokedynamic indexbyte1, indexbyte2, 0, 0 参考维基百科链接: http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
这条指令在这里对应的二进制 BA 00 18 00 00 , BA表示invokedynamic指令对应的十六进制, 十六进制(0018) = 十进制(24), 后面2个字节暂时没有用到.
indexbyte1 和 indexbyte2 这2个字节表示常量池索引, 也就是上面的 [24], 该常量池索引指向了一个CONSTANT_InvokeDynamic条目。
这个条目索引了引导方法(BootstrapMethods) 和 动态调用点相关的方法名字及方法类型(CONSTANT_NameAndType_info)。
在这里, 引导方法为LambdaMetafactory.metafactory, 方法的名字为 apply, 方法的类型为 : (Ljava/lang/String;)Ljava/util/function/Function;
(表示有一个String类型的参数, 方法返回值类型为java.util.function.Function)
再看看lambda$0的字节码, 可以看到, 就是Lambda表达式体的代码, 大概如下代码, 可以看到, 用到的局部变量hello被参数arg0替换了.
private static synthetic Void lambda$0(String arg0, String name){
System.out.println(arg0 + name);
return null;
}
编译器会将Lambda表达式的内容, 编译成当前类的一个实例或静态方法(取决于Lambda表达式出现在实例方法中还是静态方法中)
最后看看Bootstrap方法
介绍可以参考这里: http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html
http://han.guokai.blog.163.com/blog/static/13671827120118125237946 (网上找到的上面的翻译)
作用:
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法),
动态调用点依靠引导方法来链接到具体的方法. 引导方法是由编译器生成, 在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来
将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为.
引导方法的返回值类型是java.lang.invoke.CallSite, 一个invokedynamic指令关联一个CallSite, 将所有的调用委托到CallSite当前的target(MethodHandle)
参数:(说明来自api)
LambdaMetafactory.metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType)有六个参数, 按顺序描述如下
1. MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动自动填充这个参数, 这里JVM为我们填充
为Lookup(com.haogrgr.java8.main.Main.class, (PUBLIC | PRIVATE | PROTECTED | PACKAGE)) 意思是这个Lookup实例可以访问Main类的所有成员.
2. String invokedName : 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name),
在这里JVM为我们填充为 "apply", 即Function.apply方法名.
3. MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature). 使用invokedynamic指令时, JVM会自动自动填充这个参数
(填充内容来自常量池InvokeDynamic.NameAndType.Type), 在这里参数为String, 返回值类型为Function, 表示这个调用点的目标方法的参数为String,
然后invokedynamic执行完后会返回一个Function实例 ((String)Function).
4. MethodType samMethodType : 函数对象将要实现的接口方法类型, 这里运行时, 值为 (Object)Object 即 Function.apply方法的类型(泛型信息被擦除).
5. MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配,
和在调用参数前附加上捕获的参数), 在这里为 com.haogrgr.java8.main.Main.lambda$0(String,String)Void 方法的方法句柄.
6. MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为:
比如函数接口方法定义为 T apply(R r) T和R都为泛型标识, 这个时候方法类型为(Object)Object, 在编译时T和R都已确定, 这个时候具体的方法类型可能
为(String)Void 即T和R由具体的Void和String替换, 这时samMethodType就是 (Object)Object, 而instantiatedMethodType为(String)Void.
第4, 5, 6 三个参数来自class文件中的. 如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数.
Lambda表达式结果:
从这里可以看出, Lambda表达会返回一个对应接口的具体实现实例, 可以看到这里的Lambda表达式返回了一个 Function<String, Void> 的实例.
Function<String, Void> func = (name) -> {
System.out.println(hello + name);
return null;
};
那么, 前面说了, 这里Lambda表达式的内容被编译成了Main类的一个static方法, 那么这个实例是怎么回事呢? 先来看看这个实例的字节码.
JDK提供了一个参数来输出生动态生成的类的字节码: System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
还一个好玩的属性是-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true, 这个只能通过vm参数来指定了.
将上面的属性可以通过启动参数设置, 也可以在代码里设置, 如果在代码里设置, 请放在main方法第一行, 下面是上面例子中生成的类的字节码
final synthetic class com.haogrgr.java8.main.Main$$Lambda$1 implements java.util.function.Function {
// Field descriptor #8 Ljava/lang/String;
private final java.lang.String arg$1;
// Method descriptor #10 (Ljava/lang/String;)V
// Stack: 2, Locals: 2
private Main$$Lambda$1(java.lang.String arg0);
0 aload_0 [this]
1 invokespecial java.lang.Object() [13]
4 aload_0 [this]
5 aload_1 [arg0]
6 putfield com.haogrgr.java8.main.Main$$Lambda$1.arg$1 : java.lang.String [15]
9 return
// Method descriptor #17 (Ljava/lang/String;)Ljava/util/function/Function;
// Stack: 3, Locals: 1
private static java.util.function.Function get$Lambda(java.lang.String arg0);
0 new com.haogrgr.java8.main.Main$$Lambda$1 [2]
3 dup
4 aload_0 [arg0]
5 invokespecial com.haogrgr.java8.main.Main$$Lambda$1(java.lang.String) [19]
8 areturn
// Method descriptor #21 (Ljava/lang/Object;)Ljava/lang/Object;
// Stack: 2, Locals: 2
public java.lang.Object apply(java.lang.Object arg0);
0 aload_0 [this]
1 getfield com.haogrgr.java8.main.Main$$Lambda$1.arg$1 : java.lang.String [15]
4 aload_1 [arg0]
5 checkcast java.lang.String [23]
8 invokestatic com.haogrgr.java8.main.Main.lambda$0(java.lang.String, java.lang.String) : java.lang.Void [29]
11 areturn
}
下面是将上面的字节码翻译过来后的内容(Main.lambda$0()方法的反编译代码在前面有写)
package com.haogrgr.java8.main;
import java.util.function.Function;
final class Main$$Lambda$1 implements Function<String, Void> {
private final String hello;
private Main$$Lambda$1(String hello){
this.hello = hello;
}
private static Function<String, Void> get$Lambda(String hello){
return new Main$$Lambda$1(hello);
}
@Override
public Void apply(String name) {
return Main.lambda$0(this.hello, name);
}
}
这里可以看到, 因为Lambda有用到hello这个局部变量, 于是将这个局部变量的值保存在了生成的实例中的一个final属性(这是不是叫做捕获()?),
这也就说明了, 为什么用于Lambda里面的外部局部变量必须是final类型的或者不能重新赋值.
同时, 我们看到, 实例实现了Function接口, 接口方法的实现为调用Main中Lambda生成的静态方法.
整理:
现在, 我们知道了:
1.Lambda表达的内容被编译成了当前类的一个静态或实例方法.
2.Lambda表达式所在处会产生一条invokedynamic指令调用, 同时编译器会生成一个对应的Bootstrap Method.
3.当JVM第一次碰到这条invokedynamic时, 会调用对应的Bootstrap方法.
4.由Lambda表达式产生的invokedynamic指令的引导方法是调用LambdaMetafactory.metafactory()方法.
5.调用引导方法会返回一个CallSite对象实例, 该实例target引用一个MethodHandle实例.
6.执行MethodHanlde代表的方法(?), 返回结果, 结果为动态生成的接口实例, 接口实现调用1布中生成的方法.
调试思路:
断点LambdaMetafactory.metafactory()方法.
后续逻辑: 1.OSC博客字数限制, 今天先写到这里, 后面的下个星期再写, 后面大概内容, 我好人, 不留悬念~~~~
1. LambdaMetafactory.metafactory()方法逻辑主要是生成动态代理类Class字节码 和 创建CallSite,具体是ConstantCallSite子类.
2. ConstantCallSite类的target引用的MH根据情况可能为BoundMethodHandle.Species_L(当Lambda没有用到外部变量时, 一种优化)
或者 DirectMethodHandle(上面的例子就是这种, 如果没有用到hello变量时, 就会是Species_L类).
3. 2 中的MH的语义为(以前面的代码为例): 调用 Main$$Lambda$1.get$Lambda(String hello)方法来构造 1 中生成的动态类实例(略了点东西), 返回.
如果是Species_L, 则, Species_L有个属性argL0, 存放的是动态类的实例(调用引导方法时就已实例化), 然后它的语义就是 : 获取自己的argL0属性, 然后返回.
4. LambdaForm : The symbolic, non-executable form of a method handle's invocation semantics.
6. 动态生成字节码和匿名内部类两种Lambda实现方式对比. (http://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf)
7. OpenJDK上关于Lambda实现的一篇文章, 里面有介绍Lambda表达式编译期的一些处理, 包含序列化兼容等等
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
8. 选择使用IDY指令是为了以后可以方便的替换为其他实现.
9. 还会有一些比如自动装箱, 参数转换等等的高级特性, 我也没太弄明白~~~
这篇文章是我本地Debug后记录下来的所见所闻, 不一定完全正确, 如有错误, 请务必指出.
来源:oschina
链接:https://my.oschina.net/u/181985/blog/367391