Java 8 Lambdas实现原理
2017-07-04 19:48
148 查看
转载自:http://colobu.com/2014/11/06/secrets-of-java-8-lambda/
http://blog.csdn.net/raintungli/article/details/54910152
Lambda表达式到底被编译成了什么,结论是JVM会在编译时和运行时对Lambda表达式动了手脚。
编译时:Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑;生成invokedynamic指令,调用bootstrap方法,由java.lang.invoke.LambdaMetafactory.metafactory方法实现。
运行时:invokedynamic指令调用metafactory方法。 它会返回一个CallSite, 此CallSite返回目标类型的一个匿名实现类, 此类关联编译时产生的方法;lambda表达式调用时会调用匿名实现类关联的方法。
下面来个简单的lambda表达式的例子:
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。
引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来,引导方法的返回值永久的决定了调用点的行为。引导方法的返回值类型是java.lang.invoke.CallSite,一个invokedynamic指令关联一个CallSite, 将所有的调用委托到CallSite当前的target(MethodHandle)。
找到 #2 = InvokeDynamic, #0:#25 表示要执行 #0 处的指令,#0 位置即BootstrapMethods:
1.MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动自动填充这个参数
2. String invokedName : 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们填充为 "apply", 即Consumer.accept方法名.
3. MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature). 使用invokedynamic指令时, JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type), 在这里参数为String, 返回值类型为Consumer, 表示这个调用点的目标方法的参数为String, 然后invokedynamic执行完后会返回一个即Consumer实例.
4. MethodType samMethodType : 函数对象将要实现的接口方法类型, 这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除).#67 (Ljava/lang/Object;)V
5. MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数), 在这里为 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.
6. MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型。比如函数接口方法定义为 void accept(T t) T为泛型标识, 这个时候方法类型为(Object)Void, 在编译时T已确定, 即T由String替换, 这时samMethodType就是 (Object)Void, 而instantiatedMethodType为(String)Void。
第4, 5, 6 三个参数来自class文件中的,如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数:
metafactory返回的CallSite实际是由InnerClassLambdaMetafactory的buildCallSite来生成。生成之前会调用validateMetafactoryArgs方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。InnerClassLambdaMetafactory.buildCallSite方法如下:
其中spinInnerClass调用asm框架动态的产生SAM实现的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数:
现在将这个内部类生成在磁盘上,代码里加上参数后运行Test.class,在目录上会多了Test$ $Lambda$1.class文件,javap -p -v Test\$\$Lambda\$1.class:
如果,使用的是方法引用
总结:
使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法。
留个思考问题,为什么lambda只能使用表达式外面用final修饰的变量?
http://blog.csdn.net/raintungli/article/details/54910152
Lambda表达式到底被编译成了什么,结论是JVM会在编译时和运行时对Lambda表达式动了手脚。
编译时:Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑;生成invokedynamic指令,调用bootstrap方法,由java.lang.invoke.LambdaMetafactory.metafactory方法实现。
运行时:invokedynamic指令调用metafactory方法。 它会返回一个CallSite, 此CallSite返回目标类型的一个匿名实现类, 此类关联编译时产生的方法;lambda表达式调用时会调用匿名实现类关联的方法。
下面来个简单的lambda表达式的例子:
import java.util.function.Consumer; public class Test { public static void main(String[] args) { Consumer<String> c = s -> System.out.println(s); c.accept("hello lambda"); } }使用javap查看生成的字节码 javap -c -p -v Test.class:
Classfile /Test.class Last modified Jun 23, 2017; size 1057 bytes MD5 checksum eab4179ef0e9aed7da4c1dd221300e1f Compiled from "Test.java" public class Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #8.#19 // java/lang/Object."<init>":()V #2 = InvokeDynamic #0:#25 // #0:accept:()Ljava/util/function/Consumer; ...... { public Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 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 #2, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 5: astore_1 6: aload_1 7: ldc #3 // String hello lambda 9: invokeinterface #4, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V 14: return LineNumberTable: line 5: 0 line 6: 6 line 7: 14 private static void lambda$main$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 #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return LineNumberTable: line 5: 0 } SourceFile: "Test.java" InnerClasses: public static final #52= #51 of #55; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles BootstrapMethods: 0: #21 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: #22 (Ljava/lang/Object;)V #23 invokestatic Test.lambda$main$0:(Ljava/lang/String;)V #24 (Ljava/lang/String;)V第41行可以看出来,Lambda表达式体被生成一个称之为lambda$main$0的方法,它调用System.out.println输出传入的参数。在main方法中,原lambda表达式处产生了一条invokedynamic #2, 0,#2也就是后面的注释InvokeDynamic #0:accept:()Ljava/util/function/Consumer,0 是占位符号,无用。
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。
引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来,引导方法的返回值永久的决定了调用点的行为。引导方法的返回值类型是java.lang.invoke.CallSite,一个invokedynamic指令关联一个CallSite, 将所有的调用委托到CallSite当前的target(MethodHandle)。
找到 #2 = InvokeDynamic, #0:#25 表示要执行 #0 处的指令,#0 位置即BootstrapMethods:
BootstrapMethods: 0: #21 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: #22 (Ljava/lang/Object;)V #23 invokestatic Test.lambda$main$0:(Ljava/lang/String;)V #24 (Ljava/lang/String;)VLambda表达式具体的转换是通过java.lang.invoke.LambdaMetafactory.metafactory实现的, 静态参数依照lambda表达式和目标类型不同而不同。来看LambdaMetafactory.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(); }这里需要六个参数,按顺序描述如下:
1.MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动自动填充这个参数
2. String invokedName : 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们填充为 "apply", 即Consumer.accept方法名.
3. MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature). 使用invokedynamic指令时, JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type), 在这里参数为String, 返回值类型为Consumer, 表示这个调用点的目标方法的参数为String, 然后invokedynamic执行完后会返回一个即Consumer实例.
4. MethodType samMethodType : 函数对象将要实现的接口方法类型, 这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除).#67 (Ljava/lang/Object;)V
5. MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数), 在这里为 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.
6. MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型。比如函数接口方法定义为 void accept(T t) T为泛型标识, 这个时候方法类型为(Object)Void, 在编译时T已确定, 即T由String替换, 这时samMethodType就是 (Object)Void, 而instantiatedMethodType为(String)Void。
第4, 5, 6 三个参数来自class文件中的,如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数:
Method arguments: #22 (Ljava/lang/Object;)V #23 invokestatic Test.lambda$main$0:(Ljava/lang/String;)V #24 (Ljava/lang/String;)V
metafactory返回的CallSite实际是由InnerClassLambdaMetafactory的buildCallSite来生成。生成之前会调用validateMetafactoryArgs方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。InnerClassLambdaMetafactory.buildCallSite方法如下:
CallSite buildCallSite() throws LambdaConversionException { final Class<?> innerClass = spinInnerClass(); if (invokedType.parameterCount() == 0) { ..... //调用构造函数初始化一个SAM的实例 return new ConstantCallSite(MethodHandles.constant(samBase, inst)); } else { UNSAFE.ensureClassInitialized(innerClass); return new ConstantCallSite( MethodHandles.Lookup.IMPL_LOOKUP .findStatic(innerClass, NAME_FACTORY, invokedType)); } }函数spinInnerClass 构建了这个内部类,也就是生成了一个Lambda$$Lambda$1/716157500 这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数
其中spinInnerClass调用asm框架动态的产生SAM实现的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数:
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");会在指定的路径”.”即当前运行路径上生成这个内部类。这个实现类的的方法将会调用编译时产生的那个实现方法,即lambda$main$0。
现在将这个内部类生成在磁盘上,代码里加上参数后运行Test.class,在目录上会多了Test$ $Lambda$1.class文件,javap -p -v Test\$\$Lambda\$1.class:
Classfile /Test$$Lambda$1.class Last modified Jun 23, 2017; size 392 bytes MD5 checksum 4ab389fb5e490e8e0070d0b11a9c71b9 final class Test$$Lambda$1 implements java.util.function.Consumer minor version: 0 major version: 52 flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC Constant pool: #1 = Utf8 Test$$Lambda$1 …… { private Test$$Lambda$1(); descriptor: ()V flags: ACC_PRIVATE Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #10 // Method java/lang/Object."<init>":()V 4: return public void accept(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: checkcast #15 // class java/lang/String 4: invokestatic #21 // Method Test.lambda$main$0:(Ljava/lang/String;)V 7: return RuntimeVisibleAnnotations: 0: #13() }这个类implements实现了Consumer接口,accept方法实际上调用了Test.lambda$main$0静态函数,也就是表达式中的函数块。
如果,使用的是方法引用
public static void main(String[] args) throws Throwable { Consumer<String> c = System.out::println; c.accept("hello"); }这段代码是不会产生一个类似" lambda$main$0"新方法。 因为LambdaMetafactory会直接使用这个引用的方法。
总结:
使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法。
留个思考问题,为什么lambda只能使用表达式外面用final修饰的变量?
相关文章推荐
- 利用Java Reflection(反射)原理,在hibernate里面实现对单表、视图的动态组合查询
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- Java对象池技术原理及其实现
- Java对象池技术原理及其实现
- Taglib原理和实现(作者WalkingWithJava)
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- Java对象池技术的原理及其实现
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- java中使用线程实现Timer(定时器)原理和源码
- Jsp/Java代码分离.实现页面真正的代码分离 设计原理
- 快速排序原理及java实现
- Java 虚拟机类装载:原理、实现与应用
- java中使用线程实现Timer(定时器)原理和源码
- Java对象池技术的原理及其实现的小结。
- java中使用线程实现Timer(定时器)原理和源码