lambda表达式原理

lambda表达式原理

Posted by Link on November 22, 2022

lambda表达式原理

一、lambda表达式概述

Java 8于2014年3月发布,引入了lambda表达式作为其旗舰功能。你可能已经在你的代码库中使用它们来编写更简洁和灵活的代码。

比如在stream中使用,统计所有账单中7月的账单金额总和,如下。

int total = invoices.stream()
                    .filter(inv -> inv.getMonth() == Month.JULY)
                    .mapToInt(Invoice::getAmount)
                    .sum();

如果是java8之前的风格,则是使用匿名内部类,代码如下,实现的功能和lambda表达式相同。

int total = invoices.stream()
                    .filter(new Predicate<Invoice>() {
                        @Override
                        public boolean test(Invoice inv) {
                            return inv.getMonth() == Month.JULY;
                        }
                    })
                    .mapToInt(new ToIntFunction<Invoice>() {
                        @Override
                        public int applyAsInt(Invoice inv) {
                            return inv.getAmount();
                        }
                    })
                    .sum();

二、字节码与实现的对比

2.1 内部类

代码

import java.util.function.Function;
public class AnonymousClassExample {
    Function<String, Integer> format = new Function<String, Integer>() {
        public Integer apply(String input){
            return Integer.parseInt(input);
        }
    };
}

使用javac编译后生成字节码,可以看到生成了两个class文件 AnonymousClassExample.classAnonymousClassExample$1.class,后面的一个文件即为匿名内部类的class文件。匿名内部类的实现总是要创建一个匿名内部类的class文件。

javac AnonymousClassExample.java

使用javap反汇编后可以看到

javap -c AnonymousClassExample.java
public class AnonymousClassExample {
  java.util.function.Function<java.lang.String, java.lang.Integer> format;

  public AnonymousClassExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #2                  // class AnonymousClassExample$1
       8: dup
       9: aload_0
      10: invokespecial #3                  // Method AnonymousClassExample$1."<init>":(LAnonymousClassExample;)V
      13: putfield      #4                  // Field format:Ljava/util/function/Function;
      16: return
}

可以看到静态内部类的实现方式总是要创建一个类实例,5: new

2.2 lambda

2.2.1 lambda的执行过程

invokedynamic 字节码指令:运行时 JVM 第一次到某个地方的这个指令的时候会进行 linkage,会调用用户指定的 Bootstrap Method 来决定要执行什么方法,之后便不需要这个步骤。 Bootstrap Method: 用户可以自己编写的方法,最终需要返回一个 CallSite 对象。 CallSite: 保存 MethodHandle 的容器,里面有一个 target MethodHandle。 MethodHandle: 真正要执行的方法的指针,即上面的生成的内部类invokespecial, invokestatic。

import java.util.function.Function;

public class Lambda {
    Function<String, Integer> f = s -> Integer.parseInt(s);
}

同样javac编译javap反汇编后得到如下结果,其中不一样的就是lambda的实现使用了invokedynamic,在这里我们还看不到具体的执行方法。

public class Lambda {
  java.util.function.Function<java.lang.String, java.lang.Integer> f;

  public Lambda();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/Function;
      10: putfield      #3                  // Field f:Ljava/util/function/Function;
      13: return
}

使用javap -v 后可以看到具体的调用。

每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site),动态调用点最开始是未链接状态(unlinked):表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为。这一过程对应字节码中的#22(LambdaMetafactory.metafactory方法)

Constant pool:
   #2 = InvokeDynamic      #0:#26         // #0:apply:()Ljava/util/function/Function;
   //...
  #26 = NameAndType        #36:#37        // apply:()Ljava/util/function/Function;
  #36 = Utf8               apply
  #37 = Utf8               ()Ljava/util/function/Function;

BootstrapMethods:
  0: #22 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:
      #23 (Ljava/lang/Object;)Ljava/lang/Object;
      #24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
      #25 (Ljava/lang/String;)Ljava/lang/Integer;

可以看到最终使用invokestatic调用了LambdaMetafactory.metafactory。

LambdaMetafactory.metafactory方法有六个参数

  1. MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时,JVM会自动自动填充这个参数。
  2. String invokedName : 要实现的方法的名字, 使用invokedynamic时,JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name)#36, 在这里JVM为我们填充为 “apply”。
  3. MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature)。 使用invokedynamic指令时,JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type) #37
  4. MethodType samMethodType : 函数对象将要实现的接口方法类型,这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除)。#23 (Ljava/lang/Object;)V
  5. MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数)。在这里为 #24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer; 方法的方法句柄.
  6. MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型: #25 (Ljava/lang/String;)Ljava/lang/Integer;
public class LambdaMetafactory {
    //...
    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();
    }
    //...
}

InnerClassLambdaMetafactory$buildCallSite,该方法最终生成的CallSite即为5:中invokedynamic的调用点,最后通过Unsafe(java的defineAnonymousClass()方法来将字节数组转换成Class对象。

    CallSite buildCallSite() throws LambdaConversionException {
        //生成class
        final Class<?> innerClass = spinInnerClass();
        //...
        //构造CallSite
        UNSAFE.ensureClassInitialized(innerClass);
        return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
    }

    private Class<?> spinInnerClass() throws LambdaConversionException {
        //...
        UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
    }

看到最后发现还是构造了个内部类,那么lambda真的就是个语法糖么?其实还是有区别的,我们先来对比两种不同的lambda表达式,也就是是否有引用外部变量。

2.2.2 非捕获式non-capturing

non-capturing (the lambda doesn’t access any variables defined outside its body)

import java.util.function.Function;

public class Lambda {
    Function<String, Integer> f = s -> Integer.parseInt(s);
}

字节码反汇编

java -p private

public class Lambda {
  java.util.function.Function<java.lang.String, java.lang.Integer> f;
  public Lambda();
  private static java.lang.Integer lambda$new$0(java.lang.String);
}

java -c -v private

Constant pool:
   #2 = InvokeDynamic      #0:#26         // #0:apply:()Ljava/util/function/Function;
BootstrapMethods:
  0: #22 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:
      #23 (Ljava/lang/Object;)Ljava/lang/Object;
      #24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
      #25 (Ljava/lang/String;)Ljava/lang/Integer;

设置展示lambda之后可以看到

    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
final class Lambda$$Lambda$1 implements Function {
    private Lambda$$Lambda$1() {
    }

    @Hidden
    public Object apply(Object var1) {
        return Lambda.lambda$new$0((String)var1);
    }
}

对应的静态内部类实现

class AnonymousClassExample$1 implements Function<String, Integer> {
    AnonymousClassExample$1(AnonymousClassExample var1) {
        this.this$0 = var1;
    }

    public Integer apply(String var1) {
        return Integer.parseInt(var1);
    }
}

可以看到这种情况下lambda简化了,没有传实例参数这一步了,而且不同的Lambda类调用都只用这个内部类即可,不像匿名内部类每次都要创建。

2.2.3 捕获式capturing

capturing (the lambda accesses variables defined outside its body).

 import java.util.function.Function;

public class LambdaWithCapture {
    private int offset = 10;
    Function<String, Integer> f = s -> Integer.parseInt(s) + offset;
}

字节码反汇编

java -p private

 public class LambdaWithCapture {
  private int offset;
  java.util.function.Function<java.lang.String, java.lang.Integer> f;
  public LambdaWithCapture();
  private java.lang.Integer lambda$new$0(java.lang.String);
}

java -c -v private

 Constant pool:
    #3 = InvokeDynamic      #0:#30         // #0:apply:(LLambdaWithCapture;)Ljava/util/function/Function;
 BootstrapMethods:
  0: #26 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:
      #27 (Ljava/lang/Object;)Ljava/lang/Object;
      #28 invokespecial LambdaWithCapture.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
      #29 (Ljava/lang/String;)Ljava/lang/Integer;

设置展示lambda之后可以看到

    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
final class LambdaWithCapture$$Lambda$2 implements Function {
    private final LambdaWithCapture arg$1;

    private LambdaWithCapture$$Lambda$2(Lambda var1) {
        this.arg$1 = var1;
    }

    private static Function get$Lambda(Lambda var0) {
        return new LambdaWithCapture$$Lambda$2(var0);
    }

    @Hidden
    public Object apply(Object var1) {
        return this.arg$1.lambda$new$1((String)var1);
    }
}

对应的静态内部类实现

class AnonymousClassWithCapture$1 implements Function<String, Integer> {
    AnonymousClassWithCapture$1(AnonymousClassWithCapture var1) {
        this.this$0 = var1;
    }

    public Integer apply(String var1) {
        return Integer.parseInt(var1) + 12;
    }
}

这种情况两者的实现没有多大区别。

2.2.4 总结

两个lambda最大的区别就在于创建CallCite的MethodHandle时构造内部类的方式不同,一个不需要把调用的类实例传入且使用调用类生成的static方法,一个则需要传入且使用调用实例的成员方法。

三、总结

两者性能的比较可以分为三个部分

  1. 链接步骤。lambda工厂步骤与匿名内层类的类加载对比。Oracle发布了Sergey Kuksenko关于这个权衡的性能分析,你可以看到Kuksenko在2013年JVM语言峰会上发表了关于这个主题的演讲[3]。该分析表明,lambda工厂方法需要时间来预热,在这期间,它最初会比较慢。当有足够多的调用站点链接时,如果代码是在一个热的路径上(即,一个足够频繁的调用,以获得JIT编译),性能就会与类的加载一致。另一方面,如果是冷路径,lambda工厂的方法可能会快100倍。

  2. 捕捉周围范围的变量。如果没有变量需要捕获,那么这一步可以自动优化,以避免用基于lambda工厂的实现分配一个新对象。

  3. 调用实际的方法。目前,匿名内部类和lambda表达式都执行完全相同的操作,所以在这里没有性能上的差异。非捕获式lambda表达式的开箱即用的性能已经领先于悬挂式匿名内类的等价物。捕获lambda表达式的实现与为了捕获这些字段而分配一个匿名内类的性能相似。

另外lambda使用invokedynamic将lambda的实现从jvm搬到了jdk中(只需要返回一个CallCite),便于后续更改实现与拓展。