java中的 lambda表达式 & InvokeDynamic

方法分派(dispatch)

从Java 1.0到现在,invokedynamic是第一个新加入的Java字节码,它与已有的字节码invokevirtualinvokestaticinvokeinterfaceinvokespecial组合在了一起。已有的这四个操作码实现了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.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类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.classLambdaNew$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

  1. invokedynamic
        0: invokedynamic #30,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

#30 是代表常量#30 也就是后面的注释InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
0 是占位符号,目前无用

  1. 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)*有六个参数, 按顺序描述如下

  1. MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动填充这个参数

  2. String invokedName : 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们自动填充为 “accept”, 即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.

前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,再组合方法名称以及定义方法的类来查找方法句柄。


整理

现在, 我们知道了:

  1. Lambda表达的内容被编译成了当前类的一个静态方法.
  2. Lambda表达式所在处会产生一条invokedynamic指令调用, 同时编译器会生成一个对应的Bootstrap Method.
  3. 当JVM第一次碰到这条invokedynamic时, 会调用对应的Bootstrap方法.
  4. 由Lambda表达式产生的invokedynamic指令的引导方法是调用LambdaMetafactory.metafactory()方法.
  5. 调用引导方法会返回一个CallSite对象实例, 该实例target引用一个MethodHandle实例.
  6. 执行MethodHanlde代表的方法, 返回结果, 结果为动态生成的接口实例, 接口实现调用1.中生成的实例或者静态方法(取决于Lambda表达式出现在实例方法中还是静态方法中).
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱游泳的老白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值