JDK8中Lambda表达式底层实现浅析(一)

我的未来我决定 提交于 2019-12-01 23:10:45

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后记录下来的所见所闻, 不一定完全正确, 如有错误, 请务必指出.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!