Java 8 Lambda 揭秘

澳门新葡亰平台游戏网站 48

再了解了Java 8 Lambda的一些基本概念和应用后,
我们会有这样的一个问题: Lambda表达式被编译成了什么?。
这是一个有趣的问题,涉及到JDK的具体的实现。
本文将介绍OpenJDK对Lambda表达式的转换细节, 读者可以了解Java 8
Lambda表达式背景知识。

原标题:通过字节码分析JDK8中Lambda表达式编译及执行机制【面试+工作】

Lambda表达式的转换策略

Brian
Goetz是Oracle的Java语言架构师,
JSR 335(Lambda Expression)规范的lead, 写了几篇Lambda设计方面的文章,
其中之一就是Translation of Lambda
Expressions。
这篇文章介绍了Java 8 Lambda设计时的考虑以及实现方法。

他提到, Lambda表达式可以通过内部类, method handle, dynamic
proxy等方式实现, 但是这些方法各有优劣。 真正要实现Lambda表达式,
必须兼顾两个目标:
一是不引入特定策略,以期为将来的优化提供最大的灵活性,
二是保持类文件格式的稳定。 通过Java 7中引入的invokedynamic (JSR
292), 可以很好的兼顾这两个目标。

invokedynamic 在缺乏静态类型信息的情况下可以支持有效的灵活的方法调用。主要是为了日益增长的运行在JVM上的动态类型语言,
如Groovy, JRuby。

invokedynamic将Lambda表达式的转换策略推迟到运行时,
这也意味着我们现在编译的代码在将来的转换策略改变的情况下也能正常运行。

编译器在编译的时候, 会将Lambda表达式的表达式体 (lambda
body)脱糖(desugar)
成一个方法,此方法的参数列表和返回类型和lambda表达式一致,
如果有捕获参数, 脱糖的方法的参数可能会更多一些,
并会产生一个invokedynamic调用, 调用一个call site。 这个call
site被调用时会返回lambda表达式的目标类型(functional
interface)的一个实现类。 这个call site称为这个lambda表达式的lambda
factory。 lambda factory的Bootstrap方法是一个标准方法,
叫做lambda metafactory。

编译器在转换lambda表达式时,
可以推断出表达式的参数类型,返回类型以及异常,
称之为natural signature
我们将目标类型的方法签名称之为lambda descriptor, lambda
factory的返回对象实现了函数式接口, 并且关联的表达式的代码逻辑,
称之为lambda object

通过字节码分析JDK8中Lambda表达式编译及执行机制【面试+工作】

在本文中,我们将展示一些在 Java 8 中不太为人所了解的 Lambda
表达式技巧及其使用限制。本文的主要的受众是 Java
开发人员,研究人员以及工具库的编写人员。 这里我们只会使用没有 com.sun
或其他内部类的公共 Java API,如此代码就可以在不同的 JVM
实现之间进行移植。

转换举例

以上的解释有点晦涩, 简单来说

  • 编译时
    • Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑
    • 生成invokedynamic指令, 调用bootstrap方法,
      由java.lang.invoke.LambdaMetafactory.metafactory方法实现
  • 运行时
    • invokedynamic指令调用metafactory方法。 它会返回一个CallSite,
      此CallSite返回目标类型的一个匿名实现类,
      此类关联编译时产生的方法
    • lambda表达式调用时会调用匿名实现类关联的方法。

最简单的一个lambda表达式的例子:

public class Lambda1 {
    public static void main(String[] args) {
        Consumer<String> c = s -> System.out.println(s);
        c.accept("hello lambda");
    }
}

使用javap查看生成的字节码 javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class:

[root@colobu bin]# javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class 
Classfile /mnt/eclipse/Lambda/bin/com/colobu/lambda/chapter5/Lambda1.class
  Last modified Nov 6, 2014; size 1401 bytes
  MD5 checksum fe2b2d3f039a9ba4209c488a8c4b4ea8
  Compiled from "Lambda1.java"
public class com.colobu.lambda.chapter5.Lambda1
  SourceFile: "Lambda1.java"
  BootstrapMethods:
    0: #57 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:
        #58 (Ljava/lang/Object;)V
        #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #62 (Ljava/lang/String;)V
  InnerClasses:
       public static final #68= #64 of #66; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             //  com/colobu/lambda/chapter5/Lambda1
   #2 = Utf8               com/colobu/lambda/chapter5/Lambda1
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          //  java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          //  "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/colobu/lambda/chapter5/Lambda1;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = NameAndType        #17:#18        //  accept:()Ljava/util/function/Consumer;
  #17 = Utf8               accept
  #18 = Utf8               ()Ljava/util/function/Consumer;
  #19 = InvokeDynamic      #0:#16         //  #0:accept:()Ljava/util/function/Consumer;
  #20 = String             #21            //  hello lambda
  #21 = Utf8               hello lambda
  #22 = InterfaceMethodref #23.#25        //  java/util/function/Consumer.accept:(Ljava/lang/Object;)V
  #23 = Class              #24            //  java/util/function/Consumer
  #24 = Utf8               java/util/function/Consumer
  #25 = NameAndType        #17:#26        //  accept:(Ljava/lang/Object;)V
  #26 = Utf8               (Ljava/lang/Object;)V
  #27 = Utf8               args
  #28 = Utf8               [Ljava/lang/String;
  #29 = Utf8               c
  #30 = Utf8               Ljava/util/function/Consumer;
  #31 = Utf8               LocalVariableTypeTable
  #32 = Utf8               Ljava/util/function/Consumer<Ljava/lang/String;>;
  #33 = Utf8               lambda$0
  #34 = Utf8               (Ljava/lang/String;)V
  #35 = Fieldref           #36.#38        //  java/lang/System.out:Ljava/io/PrintStream;
  #36 = Class              #37            //  java/lang/System
  #37 = Utf8               java/lang/System
  #38 = NameAndType        #39:#40        //  out:Ljava/io/PrintStream;
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Methodref          #42.#44        //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #42 = Class              #43            //  java/io/PrintStream
  #43 = Utf8               java/io/PrintStream
  #44 = NameAndType        #45:#34        //  println:(Ljava/lang/String;)V
  #45 = Utf8               println
  #46 = Utf8               s
  #47 = Utf8               Ljava/lang/String;
  #48 = Utf8               SourceFile
  #49 = Utf8               Lambda1.java
  #50 = Utf8               BootstrapMethods
  #51 = Methodref          #52.#54        //  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;
  #52 = Class              #53            //  java/lang/invoke/LambdaMetafactory
  #53 = Utf8               java/lang/invoke/LambdaMetafactory
  #54 = NameAndType        #55:#56        //  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;
  #55 = Utf8               metafactory
  #56 = 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;
  #57 = MethodHandle       #6:#51         //  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;
  #58 = MethodType         #26            //  (Ljava/lang/Object;)V
  #59 = Methodref          #1.#60         //  com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
  #60 = NameAndType        #33:#34        //  lambda$0:(Ljava/lang/String;)V
  #61 = MethodHandle       #6:#59         //  invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
  #62 = MethodType         #34            //  (Ljava/lang/String;)V
  #63 = Utf8               InnerClasses
  #64 = Class              #65            //  java/lang/invoke/MethodHandles$Lookup
  #65 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #66 = Class              #67            //  java/lang/invoke/MethodHandles
  #67 = Utf8               java/lang/invoke/MethodHandles
  #68 = Utf8               Lookup
{
  public com.colobu.lambda.chapter5.Lambda1();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/colobu/lambda/chapter5/Lambda1;
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokedynamic #19,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1      
         6: aload_1       
         7: ldc           #20                 // String hello lambda
         9: invokeinterface #22,  2           // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
        14: return        
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 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);
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0       
         4: invokevirtual #41                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return        
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       8     0     s   Ljava/lang/String;
}

可以看到, Lambda表达式体被生成一个称之为lambda$0的方法。
看字节码知道它调用System.out.println输出传入的参数。

原lambda表达式处产生了一条invokedynamic #19, 0澳门新葡亰平台游戏网站,。它会调用bootstrap方法。

0: #57 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:
        #58 (Ljava/lang/Object;)V
        #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #62 (Ljava/lang/String;)V

如果Lambda表达式写成Consumer<String> c = (Consumer<String> & Serializable)s -> System.out.println(s);,
则BootstrapMethods的字节码为

BootstrapMethods:
    0: #108 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #109 (Ljava/lang/Object;)V
        #112 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #113 (Ljava/lang/String;)V
        #114 1

它调用的是LambdaMetafactory.altMetafactory,和上面的调用的方法不同。#114 1意味着要实现Serializable接口。

如果Lambda表达式写成“,则BootstrapMethods的字节码为

BootstrapMethods:
  0: #57 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #58 (Ljava/lang/Object;)V
      #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
      #62 (Ljava/lang/String;)V
      #63 2
      #64 1
      #65 com/colobu/lambda/chapter5/ABC

#63 2意味着要实现额外的接口。#64 1意味着要实现额外的接口的数量为1。

字节码的指令含义可以参考这篇文章:Java bytecode instruction
listings。

可以看到,
Lambda表达式具体的转换是通过java.lang.invoke.LambdaMetafactory.metafactory实现的,
静态参数依照lambda表达式和目标类型不同而不同。

方法调用的字节码指令

快速介绍

LambdaMetafactory.metafactory

现在我们可以重点关注以下 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();
    }

实际是由InnerClassLambdaMetafactorybuildCallSite来生成。
生成之前会调用validateMetafactoryArgs方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。

metaFactory方法的参数:

  • caller: 由JVM提供的lookup context
  • invokedName: JVM提供的NameAndType
  • invokedType: JVM提供的期望的CallSite类型
  • samMethodType: 函数式接口定义的方法的签名
  • implMethod: 编译时产生的那个实现方法
  • instantiatedMethodType: 强制的方法签名和返回类型,
    一般和samMethodType相同或者是它的一个特例

上面的代码基本上是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调用asm框架动态的产生SAM的实现类,
这个实现类的的方法将会调用编译时产生的那个实现方法。
你可以在编译的时候加上参数-Djdk.internal.lambda.dumpProxyClasses,
这样编译的时候会自动产生运行时spinInnerClass产生的类。

你可以访问OpenJDK的bug系统了解这个功能。 JDK-8023524

在Class文件中,方法调用即是对常量池(ConstantPool)属性表中的一个符号引用,在类加载的解析期或者运行时才能确定直接引用。

Lambda 表达式作为在 Java 8
中实现匿名方法的一种途径而被引入,可以在某些场景中作为匿名类的替代方案。
在字节码的层面上来看,Lambda 表达式被替换成了 invokedynamic
指令。这样的指令曾被用来创建功能接口的实现。 而单个方法则是利用 Lambda
里面所定义的代码将调用委托给实际方法。

重复的lambda表达式

下面的代码中,在一个循环中重复生成调用lambda表达式,只会生成同一个lambda对象,
因为只有同一个invokedynamic指令。

for (int i = 0; i<100; i++){
    Consumer<String> c = s -> System.out.println(s);
    System.out.println(c.hashCode());
}

但是下面的代码会生成两个lambda对象,
因为它会生成两个invokedynamic指令。

Consumer<String> c = s -> System.out.println(s);
System.out.println(c.hashCode());
Consumer<String> c2 = s -> System.out.println(s);
System.out.println(c2.hashCode());
  1. invokestatic 主要用于调用static关键字标记的静态方法
  2. invokespecial 主要用于调用私有方法,构造器,父类方法。
  3. invokevirtual
    虚方法,不确定调用那一个实现类,比如Java中的重写的方法调用。
  4. invokeinterface
    接口方法,运行时才能确定实现接口的对象,也就是运行时确定方法的直接引用,而不是解析期间。
  5. invokedynamic 这个操作码的执行方法会关联到一个动态调用点对象(Call
    Site object),这个call site 对象会指向一个具体的bootstrap
    方法(方法的二进制字节流信息在BootstrapMethods属性表中)的执行,invokedynamic指令的调用会有一个独特的调用链,不像其他四个指令会直接调用方法,在实际的运行过程也相对前四个更加复杂。结合后面的例子,应该会比较直观的理解这个指令。

例如,我们手头有如下代码:

生成的类名

既然LambdaMetafactory会使用asm框架生成一个匿名类,
那么这个类的类名有什么规律的。

Consumer<String> c = s -> System.out.println(s);
System.out.println(c.getClass().getName());
System.out.println(c.getClass().getSimpleName());
System.out.println(c.getClass().getCanonicalName());

输出结果如下:

com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680
Lambda3$$Lambda$1/640070680
com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680

类名格式如 <包名>.<类名>$$Lambda$/.

number是由一个计数器生成counter.incrementAndGet()。

后缀/<NN>中的数字是一个hash值,
那就是类对象的hash值c.getClass().hashCode()

Klass::external_name()中生成。

sprintf(hash_buf, "/" UINTX_FORMAT, (uintx)hash);

关于方法调用的其他详细的解释可以参考官方文档《The Java® Virtual Machine
Specification Java8 Edition》-2.11.8 Method Invocation and Return
Instructions。

澳门新葡亰平台游戏网站 1

直接调用生成的方法

上面提到,
Lambda表达式体会由编译器生成一个方法,名字格式如Lambda$XXX

既然是类中的实实在在的方法,我们就可以直接调用。当然,
你在代码中直接写lambda$0()编译通不过,
因为Lambda表达式体还没有被抽取成方法。

但是在运行中我们可以通过反射的方式调用。
下面的例子使用发射和MethodHandle两种方式调用这个方法。

public static void main(String[] args) throws Throwable {
    Consumer<String> c = s -> System.out.println(s);
    Method m = Lambda4.class.getDeclaredMethod("lambda$0", String.class);
    m.invoke(null, "hello reflect");
    MethodHandle mh = MethodHandles.lookup().findStatic(Lambda4.class, "lambda$0", MethodType.methodType(void.class, String.class));
    mh.invoke("hello MethodHandle");
}

lambda表达式运行机制

这段代码被 Java 编译器翻译过来就成了下面这样:

捕获的变量等价于’final’

我们知道,在匿名类中调用外部的参数时,参数必须声明为final

Lambda体内也可以引用上下文中的变量,变量可以不声明成final的,但是必须等价于final

下面的例子中变量capturedV等价与final, 并没有在上下文中重新赋值。

public class Lambda5 {
    String greeting = "hello";

    public static void main(String[] args) throws Throwable {

        Lambda5 capturedV = new Lambda5();
        Consumer<String> c = s -> System.out.println(capturedV.greeting + " " + s);
        c.accept("captured variable");
        //capturedV = null; //Local variable capturedV defined in an enclosing scope must be final or effectively final
        //capturedV.greeting = "hi";
    }
}

如果反注释capturedV = null;编译出错,因为capturedV在上下文中被改变。

但是如果反注释capturedV.greeting = "hi"; 则没问题,
因为capturedV没有被重新赋值, 只是它指向的对象的属性有所变化。

在看字节码细节之前,先来了解一下lambda表达式如何脱糖(desugar)。lambda的语法糖在编译后的字节流Class文件中,会通过invokedynamic指令指向一个bootstrap方法(下文中部分会称作“引导方法”),这个方法就是java.lang.invoke.LambdaMetafactory中的一个静态方法。通过debug的方式,就可以看到该方法的执行,此方法源码如下:

澳门新葡亰平台游戏网站 2

方法引用

public static void main(String[] args) throws Throwable {

    Consumer<String> c  = System.out::println;
    c.accept("hello");
}

这段代码不会产生一个类似”Lambda$0″新方法。
因为LambdaMetafactory会直接使用这个引用的方法。

BootstrapMethods:
  0: #51 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:
      #52 (Ljava/lang/Object;)V
      #59 invokevirtual java/io/PrintStream.println:(Ljava/lang/String;)V
      #60 (Ljava/lang/String;)V

#59指示实现方法为System.out::println

澳门新葡亰平台游戏网站 3

invokedynamic 指令可以用 Java 代码粗略的表示成下面这样:

在运行时期,虚拟机会通过调用这个方法来返回一个CallSite(调用点)对象。简述一下方法的执行过程,首先,初始化一个InnerClassLambdaMetafactory对象,这个对象的buildCallSite方法会将Lambda表达式先转化成一个内部类,这个内部类是MethodHandles.Lookup
caller的一个内部类,也即包含此Lambda表达式的类的内部类。这个内部类是通过字节码生成技术(jdk.internal.org.objectweb.asm)生成,再通过UNSAFE类加载到JVM。然后再返回绑定此内部类的CallSite对象,这个过程的源码也可以看一下:

澳门新葡亰平台游戏网站 4

澳门新葡亰平台游戏网站 5

正如你所看见的,LambdaMetafactory
被用来生成一个调用站点,用目标方法句柄来表示一个工厂方法。这个工厂方法使用了
invokeExact 来返回功能接口的实现。如果 Lambda 封装了变量,则 invokeExact
会接收这些变量拿来作为实参。

澳门新葡亰平台游戏网站 6

在 Oracle 的 JRE 8 中,metafactory 会利用 ObjectWeb Asm 来动态地生成
Java 类,其实现了一个功能接口。 如果 Lambda
表达式封装了外部变量,生成的类里面就会有额外的域被添加进来。这种方法类似于
Java 语言中的匿名类 —— 但是有如下区别:

这个过程将生成一个代表lambda表达式信息的内部类(也就是方法第一行的innerClass,这个类是一个
functional 类型接口的实现类),这个内部类的Class字节流是通过jdk asm
的ClassWriter,MethodVisitor,生成,然后再通过调用Constructor.newInstance方法生成这个内部类的对象,并将这个内部类对象绑定给一个MethodHandle对象,然后这个MethodHandle对象传给CallSite对象(通过CallSite的构造函数赋值)。所以这样就完成了一个将lambda表达式转化成一个内部类对象,然后将内部类通过MethodHandle绑定到一个CallSite对象。CallSite对象就相当于lambda表达式的一个勾子。而invokedynamic指令就链接到这个CallSite对象来实现运行时绑定,也即invokedynamic指令在调用时,会通过这个勾子找到lambda所代表的一个functional接口对象(也即MethodHandle对象)。所以lambda的脱糖也就是在运行期通过bootstrap
method的字节码信息,转化成一个MethodHandle的过程。

  • 匿名类是在编译时由 Java 编译器生成的。
  • Lambda 实现的类则是由 JVM 在运行时生成。

通过打印consumer对象的className(greeter.getClass().getName())可以得到结果是eight.Functionnal$$Lambda$1/659748578前面字符是Lambda表达式的ClassName,后面的659748578是刚才所述内部类的hashcode值。

metafactory 的如何实现要看是什么 JVM 供应商和版本

下面通过具体的字节码指令详细分析一下lambda的脱糖机制,并且看一下invokedynamic指令是怎么给lambda在JVM中的实现带来可能。如果前面所述过程还有不清晰,还可以参考下Oracle工程师在设计java8
Lambda表达式时候的一些思考:Translation of Lambda Expressions

当然,invokedynamic 指令并不是专门给 Java 中的 lambda
表达式来使用的。引入该指令主要是为了可以在 JVM 之上运行的动态语言。Java
所提供的 Nashorn JavaScript 引擎开箱即用,就大大地利用了该指令。

lambda表达式字节码指令示例分析

在本文的后续内容中,我们将重点介绍 LambdaMetafactory
类及其功能。本文的下一节将假设你已经完全了解了 metafactory
方法如何工作以及 MethodHandle 是什么。

先看一个简单的示例,示例使用了java.util.function包下面的Consumer。

Lambdas 小技巧

示例代码:(下面的Person对象只有一个String类型属性:name,以及一个有参构造方法)

在本节中,我们将介绍如何使用 lambdas 动态构建日常任务。

澳门新葡亰平台游戏网站 7

检查异常和 Lambdas

用verbose命令看一下方法主体的字节码信息,这里暂时省略常量池信息,后面会在符号引用到常量池信息的地方具体展示。

我们都知道,Java 提供的所有函数接口不支持检查异常。检查与未检查异常在
Java 中打着持久战。

澳门新葡亰平台游戏网站 8

如果你想使用与 Java Streams 结合使用的 lambdas 内的检查异常的代码呢?
例如,我们需要将字符串列表转换成 URL 列表,如下所示:

invokedynamic指令特性

澳门新葡亰平台游戏网站 9

可以看到第一条指令就是代表了lambda表达式的实现指令,invokedynamic指令,这个指令是JSR-292开始应用的规范,而鉴于兼容和扩展的考虑(可以参考Oracle工程师对于使用invokedynamic指令的原因),JSR-337通过这个指令来实现了lambda表达式。也就是说,只要有一个lambda表达式,就会对应一个invokedynamic指令。

URL(String)已经在 throws 地方声明了一个检查的异常,因此它不能直接用作
Function 的方法引用。 

先看一下第一行字节码指令信息

你说“是的,这里可以使用这样的技巧”:

0: invokedynamic #2, 0

澳门新葡亰平台游戏网站 10

  1. 0: 代表了在方法中这条字节码指令操作码(Opcode)的偏移索引。
  2. invokedynamic就是该条指令的操作码助记符。
  3. #2, 0
    是指令的操作数(Operand),这里的#2表示操作数是一个对于Class常量池信息的一个符号引用。逗号后面的0
    是invokedynamic指令的默认值参数,到目前的JSR-337规范版本一直而且只能等于0。所以直接看一下常量池中#2的信息。
    invokedynamic在常量是有专属的描述结构的(不像其他方法调用指令,关联的是CONSTANT_MethodType_info结构)。
    invokedynamic
    在常量池中关联一个CONSTANT_InvokeDynamic_info结构,这个结构可以明确invokedynamic指令的一个引导方法(bootstrap
    method),以及动态的调用方法名和返回信息。

这是一个很挫的做法。原因如下:

常量池索引位置#2的信息如下:

  • 使用 try-catch 块
  • 重新抛出异常
  • Java 中类型擦除的使用不足

结合CONSTANT_InvokeDynamic_info的结构信息来看一下这个常量池表项包含的信息。

这个问题被使用以下方式可以更“合法”的方式解决:

CONSTANT_InvokeDynamic_info结构如下:

  • 检查的异常仅由 Java 编程语言的编译器识别
  • throws 部分只是方法的元数据,在 JVM 级别没有语义含义
  • 检查和未检查的异常在字节码和 JVM 级别是不可区分的

澳门新葡亰平台游戏网站 11

解决的办法是只把 Callable.call 的调用封装在不带 throws 部分的方法之中:

简单解释下这个CONSTANT_InvokeDynamic_info的结构:

澳门新葡亰平台游戏网站 12

  • tag:
    占用一个字节(u1)的tag,也即InvokeDynamic的一个标记值,其会转化成一个字节的tag值。可以看一下jvm
    spec中,常量池的tag值转化表(这里tag值对应=18):

这段代码不会被 Java 编译器编译通过,因为方法 Callable.call 在其 throws
部分有受检异常。但是我们可以使用动态构造的 lambda 表达式擦除这个部分。

澳门新葡亰平台游戏网站 13

首先,我们要声明一个函数式接口,没有 throws 部分但能够委派调用给
Callable.call:

  • bootstrap_method_attr_index:指向bootstrap_methods的一个有效索引值,其结构在属性表的
    bootstrap method
    结构中,也描述在Class文件的二进制字节流信息里。下面是对应索引 0
    的bootstrap method 属性表的内容:

澳门新葡亰平台游戏网站 14

澳门新葡亰平台游戏网站 15

第二步是使用 LambdaMetafactory 创建这个接口的实现,以及委派
SilentInvoker.invoke 的方法调用给方法
Callable.call。如前所述,在字节码的级别上 throws 部分被忽略,因此,方法
SilentInvoker.invoke 能够调用方法 Callable.call 而无需声明受检异常:

这段字节码信息展示了,引导方法就是LambdaMetafactory.metafactory方法。对照着前面LambdaMetafactory.metafactory的源码一起阅读。通过debug先看一下这个方法在运行时的参数值:

澳门新葡亰平台游戏网站 16

澳门新葡亰平台游戏网站 17

第三,写一个实用方法,调用 Callable.call 而不声明受检异常:

这个方法的前三个参数都是由JVM自动链接Call
Site生成。方法最后返回一个CallSite对象,对应invokedynamic指令的操作数。

澳门新葡亰平台游戏网站 18


name_and_type_index:代表常量池表信息的一个有效索引值,其指向的常量池属性表结构一定是一个CONSTANT_NameAndType_info属性,代表了方法名称和方法描述符信息。再沿着
#44索引看一下常量池相关项的描述内容:

现在,我们可以毫无顾忌地重写我们的流,使用异常检查:

澳门新葡亰平台游戏网站 19

澳门新葡亰平台游戏网站 20

通过以上几项,可以很清楚得到invokedynamic的方法描述信息。

此代码将成功编译,因为 callUnchecked
没有被声明为需要检查异常。此外,使用单态内联缓存时可以内联式调用此方法,因为在
JVM 中只有一个实现 SilentInvoker 接口的类。

其余字节码指令解析

如果实现的 Callable.call
在运行时抛出一些异常,只要它们被捕捉到就没什么问题。

综上,已经介绍了lombda表达式在字节码上的实现方式。其他指令,如果对字节码指令感兴趣可以继续阅读,已经了解的可以略过,本小节和lambda本身没有太大关联。

澳门新葡亰平台游戏网站 21

  1. 第二条指令:5: astore_1
    指令起始偏移位置是5,主要取决于前面一个指令(invokedynamic)有两个操作数,每个操作数占两个字节(u2)空间,所以第二条指令就是从字节偏移位置5开始(后续的偏移地址将不再解释)。此指令执行后,当前方法的栈帧结构如下(注:此图没有画出当前栈帧的动态链接以及返回地址的数据结构,图中:左侧局部变量表,右侧操作数栈):

尽管有这样的方法来实现功能,但还是推荐下面的用法:

澳门新葡亰平台游戏网站 22

只有当调用代码保证不存在异常时,才能隐藏已检查的异常,才能调用相应的代码。

这里为了画图方便,所以按照局部变量表和操作数栈的实际分配空间先画出了几个格子。因为字节码信息中已经告知了[stack=4, locals=2, args_size=1]。也就是局部变量表的实际运行时空间最大占用两个Slot(一个Slot一个字节,long,double类型变量需占用两个slot),操作数栈是4个slot,参数占一个slot。这里的args是main方法的String[]
args参数。因为是个static方法,所以也没有this变量的aload_0 指令。

下面的例子演示了这种方法:

  1. 第三条: 6: aload_1将greeter 弹出局部变量表,压入操作数栈。

澳门新葡亰平台游戏网站 23 

澳门新葡亰平台游戏网站 24

这个方法是这个工具的完整实现,在这里它作为开源项目SNAMP的一部分。

  1. 第四条:7: new #3初始化person对象指令,这里并不等同于new关键字,new操作码只是找到常量池的符号引用,执行到此行命令时,运行时堆区会创建一个有默认值的对象,如果是Object类型,那么默认值是null,然后将这个对于默认值的引用地址压入到操作数栈。其中#3操作数指向的常量池Class属性表的一个引用,可以看到这个常量池项为:#3
    = Class #45 // eight/Person。此时的运行时栈帧结构如下:

使用 Getter 和 Setter

澳门新葡亰平台游戏网站 25

这一节对不同数据格式(如 JSON,Thrift
等等)的序列化/反序列化的编写工具有用。此外,如果你的代码严重依赖了为
JavaBean 的 getter 和 setter 准备的 Java 反射,那么它及其有用。

  1. 第五条:10: dup复制操作数栈栈顶的值,并且将该值入操作数栈栈顶。dup指令是一种对于初始化过程的编译期优化。因前面的new操作码并不会真正的创建对象,而是push一个引用到操作数栈,所以dup之后,这个栈顶的复制引用就可以用来给调用初始化方法(构造函数)的invokespecial提供操作数时消耗掉,同时原有的引用值就可以给其他比如对象引用的操作码使用。此时栈帧结构如下图:

getter 是在 JavaBean 中的一个使用 getXXX 命名的无参且非 Void
返回类型的方法、setter 是在 JavaBea n中的一个使用 setXXX
命名的有一个单独参数并返回 void
类型的方法。这两个符号可以被表示为函数的接口:

澳门新葡亰平台游戏网站 26

  • 一个 getter 可以被表示为一个函数的参数是 this 引用的 Function 。
  • 一个 setter 可以被表示为一个第一个参数是 this
    引用,第而个参数是被传进 setter 的值的 BiConsumer。
  1. 第六条:11: ldc #4将运行时常量池的值入操作数栈,这里的值是Lambda字符串。#4在常量池属性表中结构信息如下:

现在,我们创建两个方法,这两个方法可以把任何 getter 或 setter
转换为这些函数的接口。无论这两个函数接口是不是通用。只要类型擦除掉之后,真实的类型都等于一个对象。自动的构造一个返回类型和可以被
LambdaMetafactory 识别的参数。此外,uava’s Cache 帮助为相同的 getter 或
setter 缓存 lambdas 。

澳门新葡亰平台游戏网站 27

首先,必须为 getter 和 setter 声明一个缓存。从 Reflection API
上看,Method 表示一个真实的 getter 或 setter,并且做为一个 Key
被使用。在缓存中的值代表对于特定的 getter 或 setter 的动态构造函数接口。

此时运行时栈帧结构如下:

澳门新葡亰平台游戏网站 28

澳门新葡亰平台游戏网站 29

其次,创建工厂方法通过从方法句柄中指向一个 getter 或 setter
来创建一个函数接口的实例:

  1. 第七条:13: invokespecial #5初始化Person对象的指令(#5指向了常量池Person的初始化方法eight/Person.””:(Ljava/lang/String;)V),也即调用Person构造函数的指令。此时”Lambda”常量池的引用以及
    dup复制的person引用地址出操作数栈。这条指令执行之后,才在堆中真正创建了一个Person对象。此时栈帧结构如下:

澳门新葡亰平台游戏网站 30

澳门新葡亰平台游戏网站 31

通过 samMethodType 和 instantiatedMethodType(分别为方法 metafactory
的第三个和第五个参数)之间的区别,可以实现类型擦除后的函数接口中基于对象的参数和实际参数类型之间的自动转换以及
getter 或 setter 中的返回类型。实例化的方法类型是提供专门的 lambda
的方法实现。

7.第八条:16: invokeinterface #6,
2调用了Consumer的accept接口方法{greeter.accept(person)}。#6逗号后面的参数2是invokeinterface指令的参数,含义是接口方法的参数的个数加1,因为accpet方法只有一个参数,所以这里是1+1=2。接着再看一下常量池项
#6属性表信息:

然后,为这些工厂具有缓存的支持,创建一个门面:

澳门新葡亰平台游戏网站 32

澳门新葡亰平台游戏网站 33

以上可以看出Consumer接口的泛型被擦除(编译期间进行,所以字节码信息中并不会包含泛型信息),所以这里并不知道实际的参数操作数类型。但是这里可以得到实际对象的引用值,这里accept方法执行,greeter和person引用出栈,如下图:

作为使用 Java 反射 API 的 Method 实例,获取的方法信息可以轻松地转换为
MethodHandle。考虑到实例方法总是有隐藏的第一个参数用于将其传递给方法。静态方法没有这些隐藏的参数。例如,Integer.intValue()方法具有
int intValue 的实际签名(Integer this)。这个技巧用于实现 getter 和 setter
的功能包装器。

澳门新葡亰平台游戏网站 34

现在是时候测试代码了:

  1. 第九条:21: return方法返回,因为是void方法,所以就是opcode就是return。此时操作数栈和局部变量表都是空,方法返回。最后再画上一笔:

澳门新葡亰平台游戏网站 35

澳门新葡亰平台游戏网站 36

这种使用缓存的 getter 和 setter 的方法可以在诸如 Jackson
这样的序列化和反序列化库中高效的使用,这些库在序列化/反序列化库的过程中使用
getter 和 setter。

结语

使用 LambdaMetafactory 来动态生成的实现调用函数接口比通过 Java
Reflection API 的调用快 得多

本文只是通过Consumer接口分析lambda表达式的字节码指令,以及运行时的脱糖过程。也是把操作码忘得差不多了,也顺便再回顾一下。

你可以在这里找到完整的代码,它是开源项目 SNAMP 的一部分。

从字节码看lambda可以追溯到源头,所以也就能理解运行时的内存模型。

限制和缺陷

lambda表达式对应一个incokedynamic指令,通过指令在常量池的符号引用,可以得到BootstrapMethods属性表对应的引导方法。在运行时,JVM会通过调用这个引导方法生成一个含有MethodHandle(CallSite的target属性)对象的CallSite作为一个Lambda的回调点。Lambda的表达式信息在JVM中通过字节码生成技术转换成一个内部类,这个内部类被绑定到MethodHandle对象中。每次执行lambda的时候,都会找到表达式对应的回调点CallSite执行。一个CallSite可以被多次执行(在多次调用的时候)。如下面这种情况,只会有一个invokedynamic指令,在comparator调用comparator.compare或comparator.reversed方法时,都会通过CallSite找到其内部的MethodHandle,并通过MethodHandle调用Lambda的内部表示形式LambdaForm。

在本节中,我们将给出在 Java 编译器和 JVM 中与 lambdas
相关的一些错误和限制。 所有这些限制都可以在 OpenJDK 和 Oracle JDK
上重现,它们适用于 Windows 和 Linux 的 javac 1.8.0_131。

澳门新葡亰平台游戏网站 37

从方法句柄构建 Lambdas

Lambda不仅用起来很方便,性能表现在多数情况也比匿名内部类好,性能方面可以参考一下Oracle的Sergey
Kuksenko发布的 Lambda 性能报告。由上文可知,虽然在运行时需要转化Lambda
Form(见MethodHandle的form属性生成过程),并且生成CallSite,但是随着调用点被频繁调用,通过JIT编译优化等,性能会有明显提升。并且,运行时脱糖也增强了编译期的灵活性(其实在看字节码之前,一直以为Lambda可能是在编译期脱糖成一个匿名内部类的Class,而不是通过提供一个boortrap方法,在运行时链接到调用点)。运行时生成调用点的方式实际的内存使用率在多数情况也是低于匿名内部类(java8
之前版本的写法)的方式。所以,在能使用lambda表达式的地方,我们尽量结合实际的性能测试情况,写简洁的表达式,尽量减少Lambda表达式内部捕获变量(因为这样会创建额外的变量对象),如果需要在表达式内部捕获变量,可以考虑是否可以将变量写成类的成员变量,也即尽量少给Lambda传多余的参数。希望本文能给Lambda的使用者一些参考。返回搜狐,查看更多

如你所知,可以使用 LambdaMetafactory 动态构建
lambda。要实现这一点,你应该指定一个
MethodHandle,其中包含一个由函数接口声明的单个方法的实现。我们来看看这个简单的例子:

责任编辑:

澳门新葡亰平台游戏网站 38

上面代码等价于: 

澳门新葡亰平台游戏网站 39

但如果我们用一个可以表示一个字段获取方法的方法处理器来替换指向 getValue
的方法处理器的话,情况会如何呢:

澳门新葡亰平台游戏网站 40

该代码应该是可以按照预期来运行的,因为 findGetter
会返回一个指向字段获取方法、并且具备有效签名的方法处理器。
但是如果你运行了代码,就会看到如下异常:

澳门新葡亰平台游戏网站 41

有趣的是,如果我们使用
MethodHandleProxies,字段获取方法却可以运行得很好:

澳门新葡亰平台游戏网站 42

要注意 MethodHandleProxies 并非动态创建 lambda
表达式的理想方法,因为这个类只是把 MethodHandle
封装到一个代理类里面,然后把对 InvocationHandler.invoke 的调用指派给了
MethodHandle.invokeWithArguments 方法。 这种方法使得 Java
反射机制运行起来非常的慢。

如前所述,并不是所有的方法句柄都可以在运行时用于构建 lambdas。

只有几种与方法相关的方法句柄可以用于 lambda 表达式的动态构造

这包括:

  • REF_invokeInterface: 对于接口方法可通过 Lookup.findVirtual 来构建
  • REF_invokeVirtual: 对于由类提供的虚方法可以通过 Lookup.findVirtual
    来构建
  • REF_invokeStatic: 对于静态方法可通过 Lookup.findStatic 构建
  • REF_newInvokeSpecial: 对于构造函数可通过 Lookup.findConstructor
    构建
  • REF_invokeSpecial: 对于私有方法和由类提供的早绑定的虚方法可通过
    Lookup.findSpecial 构建

其他方法的句柄将会触发 LambdaConversionException 异常。

泛型异常

这个 bug 与 Java 编译器以及在 throws
部分声明泛型异常的能力有关。下面的示例代码演示了这种行为:

澳门新葡亰平台游戏网站 43

这段代码应该编译成功因为 URL 构造器抛出
MalformedURLException。但事实并非如此。编译器产生以下错误消息:

澳门新葡亰平台游戏网站 44

但如果我们用一个匿名类替换 lambda 表达式,那么代码就编译成功了:

澳门新葡亰平台游戏网站 45

结论很简单:

当与 lambda 表达式配合使用时,泛型异常的类型推断不能正确工作。

泛型边界

一个带有多个边界的泛型可以用 & 号构造:<T extends A & B & C & …
Z>。这种泛型参数定义很少被使用,但由于其局限性,它对 Java 中的 lambda
表达式有某些影响:

  • 每一个边界,除了第一个边界,都必须是一个接口。
  • 具有这种泛型的类的原始版本只考虑了约束中的第一个边界。

第二个局限性使 Java 编译器在编译时和 JVM 在运行时产生不同的行为,当
Lambda 表达式的联动发生时。可以使用以下代码重现此行为:

澳门新葡亰平台游戏网站 46

这段代码绝对没错,而且用 Java 编译器编译也会成功。MutableInteger
这个类可以满足泛型 T 的多个类型绑定约束:

  • MutableInteger 是从 Number 继承的
  • MutableInteger 实现了 IntSupplier

但是在运行的时候会抛出异常:

澳门新葡亰平台游戏网站 47

之所以会这样是因为 Java Stream 的管道只捕获到了一个原始类型,它是一个
Number 类。Number 类本身并没有实现 IntSupplier 接口。
要修复此问题,可以在一个作为方法引用的单独方法中明确定义一个参数类型:

澳门新葡亰平台游戏网站 48

这个示例就演示了 Java 编译器和运行时所进行的一次不正确的类型推断。

在 Java 中的编译时和运行时处理与 lambdas
结合的多个类型绑定会导致不兼容。

【编辑推荐】

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图