Lambda表达式原理解读

909 阅读2分钟

前言

最近在浏览博客时偶然了解了Lambda表达式的底层原理,对于这个日常使用的东西,自己还是了解得太浅了,所以记录了一下

结论先行

先忽略中间的细节,看看Lambda表达式是怎么实现的,随便写一段使用了Lambda表达式的代码

public class LambdaTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println("lambda");
        };
        r.run();
    }
}

启动前加上下面的配置

-Djdk.internal.lambda.dumpProxyClasses=.

image.png 如下文,在当前目录下会生成这样的一个类,run()方法中直接调用LambdaTest的静态方法lambdamainmain0(),问题来了,LambdaTest是我们写的类,明明没有lambdamainmain0()这个静态方法,是怎么蹦出来的,很容易就想到是编译的时候自动生成的,类都可以自动生成,何况是方法,那么这个方法长什么样子?

final class LambdaTest$$Lambda$1 implements Runnable {
    private LambdaTest$$Lambda$1() {
    }
​
    @Hidden
    public void run() {
        LambdaTest.lambda$main$0();
    }
}

只能看看字节码了,字节码是照妖镜,所有的妖魔鬼怪在字节码面前都无所遁形,用下面这条指令可以看到类中的所有方法

javap -c -p LambdaTest
public class com.example.aop.lambda.LambdaTest {
  // 构造器
  public com.example.aop.lambda.LambdaTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  // 我们写的main方法
  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       5: astore_1
       6: aload_1
       7: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
      12: return
  // 生成的静态方法
  // 有些同学可能不太熟悉字节码指令,但是看旁边的注释,应该隐约可以猜到,这些字节码指令对应的代码是
  // System.out.println("lambda");
  private static void lambda$main$0();
    Code:
       0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #5                  // String lambda
       5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

我们可以先得出这样的结论:使用Lambda表达式时,Lambda表达式中的内容会被编译成静态方法,并且会动态生成一个类调用这个静态方法

那么这个类和静态方法是怎么生成的,这个类是怎么调用这个静态方法的?

MethodHandle

MethodHandle又称为方法句柄,它的出现使得Java可以像其他语言一样把函数当作参数进行传递,下面用一个例子看看它是怎么使用的

public class MethodHandleDemo {
​
    public void print(String s) {
        System.out.println("hello, " + s);
    }
​
    public static void main(String[] args) throws Throwable {
        // 创建MethodType对象,指定方法的返回值类型和参数类型
        MethodType methodType = MethodType.methodType(void.class, String.class);
​
        // MethodHandles.lookup()静态方法返回 MethodHandles.Lookup 对象,这个对象表示查找的上下文
        // findVirtual查找方法签名为methodType的方法句柄
        MethodHandle methodHandle = MethodHandles.lookup().findVirtual(MethodHandleDemo.class, "print", methodType);
        // 进行方法调用
        MethodHandleDemo methodHandleDemo = new MethodHandleDemo();
        methodHandle.invokeExact(methodHandleDemo, "world");
    }
​
}

image.png

MethodHandle使用起来跟反射很像,与反射相比,MethodHandle更加灵活和轻量,方法句柄的权限检查是在句柄的创建阶段完成的,在实际调用过程中,Java虚拟机不会检查方法句柄的权限。如果该句柄被多次调用,将省下重复权限检查的开销

上文所说的新生成的类对静态方法的调用就是使用了MethodHandle

invokedynamic和BootstrapMethods

javap -v LambdaTest

使用这个指令看看上面那个例子详细版本的字节码

public class com.example.aop.lambda.LambdaTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#26         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#31         // #0:run:()Ljava/lang/Runnable;
   #3 = InterfaceMethodref #32.#33        // java/lang/Runnable.run:()V
   #4 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #36            // lambda
   #6 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #39            // com/example/aop/lambda/LambdaTest
   #8 = Class              #40            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/example/aop/lambda/LambdaTest;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               r
  #21 = Utf8               Ljava/lang/Runnable;
  #22 = Utf8               MethodParameters
  #23 = Utf8               lambda$main$0
  #24 = Utf8               SourceFile
  #25 = Utf8               LambdaTest.java
  #26 = NameAndType        #9:#10         // "<init>":()V
  #27 = Utf8               BootstrapMethods
  #28 = MethodHandle       #6:#41         // 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;
  #29 = MethodType         #10            //  ()V
  #30 = MethodHandle       #6:#42         // invokestatic com/example/aop/lambda/LambdaTest.lambda$main$0:()V
  #31 = NameAndType        #43:#44        // run:()Ljava/lang/Runnable;
  #32 = Class              #45            // java/lang/Runnable
  #33 = NameAndType        #43:#10        // run:()V
  #34 = Class              #46            // java/lang/System
  #35 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
  #36 = Utf8               lambda
  #37 = Class              #49            // java/io/PrintStream
  #38 = NameAndType        #50:#51        // println:(Ljava/lang/String;)V
  #39 = Utf8               com/example/aop/lambda/LambdaTest
  #40 = Utf8               java/lang/Object
  #41 = Methodref          #52.#53        // 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;
  #42 = Methodref          #7.#54         // com/example/aop/lambda/LambdaTest.lambda$main$0:()V
  #43 = Utf8               run
  #44 = Utf8               ()Ljava/lang/Runnable;
  #45 = Utf8               java/lang/Runnable
  #46 = Utf8               java/lang/System
  #47 = Utf8               out
  #48 = Utf8               Ljava/io/PrintStream;
  #49 = Utf8               java/io/PrintStream
  #50 = Utf8               println
  #51 = Utf8               (Ljava/lang/String;)V
  #52 = Class              #55            // java/lang/invoke/LambdaMetafactory
  #53 = NameAndType        #56:#60        // 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;
  #54 = NameAndType        #23:#10        // lambda$main$0:()V
  #55 = Utf8               java/lang/invoke/LambdaMetafactory
  #56 = Utf8               metafactory
  #57 = Class              #62            // java/lang/invoke/MethodHandles$Lookup
  #58 = Utf8               Lookup
  #59 = Utf8               InnerClasses
  #60 = 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;
  #61 = Class              #63            // java/lang/invoke/MethodHandles
  #62 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #63 = Utf8               java/lang/invoke/MethodHandles
{
  public com.example.aop.lambda.LambdaTest();
    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 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/aop/lambda/LambdaTest;
​
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: astore_1
         6: aload_1
         7: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
        12: return
      LineNumberTable:
        line 12: 0
        line 15: 6
        line 16: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            6       7     1     r   Ljava/lang/Runnable;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "LambdaTest.java"
InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #28 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:
      #29 ()V
      #30 invokestatic com/example/aop/lambda/LambdaTest.lambda$main$0:()V
      #29 ()V

只看方法相关的字节码,有一个指令比较特殊——invokedynamic

每一个invokedynamic指令的实例叫做一个动态调用点,动态调用点最开始是未链接状态,表示还未指定该调用点要调用的方法,依靠引导方法来链接到具体的方法,比如上文的invokedynamic会查找常量池中的#2,#2会查找#0,#0是一个特殊的查找,对应BootstrapMethods中的0行,可以看到这是对静态方法java.lang.invoke.LambdaMetafactory#metafactory的调用,它的返回值是java.lang.invoke.CallSite对象,这个对象的getTarget方法返回了目标方法句柄。核心的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();
}

这里的几个入参都挺有意思的,跟字节码中的NameAndType类型常量很像

  • caller:表示JVM提供的查找上下文,此处为com.example.aop.lambda.LambdaTest
  • invokeName:调用的函数名
  • invokedType:期望的方法参数的类型和返回值类型,此处为()Runnable
  • samMethodType:函数式接口定义的方法签名(参数类型和返回值类型),此处为 ()void
  • implMethod:新生成的类实际调用的方法,此处为com.example.aop.lambda.LambdaTest.lambdamainmain0()
  • instantiatedMethodType:一般和samMethodType是一样的,但在函数式接口为泛型的情况下,这里是实际的参数类型和返回值类型,比如Consumer接口,入参为泛型,如果实际情况下,入参为String,那么此处就是 (String)void

深入进去InnerClassLambdaMetafactory这个类看看,会发现是使用了ASM框架生成新的类

public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
                                   MethodType invokedType,
                                   String samMethodName,
                                   MethodType samMethodType,
                                   MethodHandle implMethod,
                                   MethodType instantiatedMethodType,
                                   boolean isSerializable,
                                   Class<?>[] markerInterfaces,
                                   MethodType[] additionalBridges)
        throws LambdaConversionException {
    super(caller, invokedType, samMethodName, samMethodType,
          implMethod, instantiatedMethodType,
          isSerializable, markerInterfaces, additionalBridges);
    implMethodClassName = implDefiningClass.getName().replace('.', '/');
    implMethodName = implInfo.getName();
    implMethodDesc = implMethodType.toMethodDescriptorString();
    implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
            ? implDefiningClass
            : implMethodType.returnType();
    constructorType = invokedType.changeReturnType(Void.TYPE);
    // 生成的类名
    lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
    // 熟悉的ClassWriter
    cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    int parameterCount = invokedType.parameterCount();
    if (parameterCount > 0) {
        argNames = new String[parameterCount];
        argDescs = new String[parameterCount];
        for (int i = 0; i < parameterCount; i++) {
            argNames[i] = "arg$" + (i + 1);
            argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
        }
    } else {
        argNames = argDescs = EMPTY_STRING_ARRAY;
    }
}

image.png

public class CallSite {
    // CallSite中有一个getTarget方法,可以获取MethodHandle,对真正要执行的方法进行调用
    public abstract MethodHandle getTarget();
}

 

总结

image.png

  1. Lambda表达式声明的地方会生成一个invokedynamic指令,同时编译器生成一个对应的引导方法BootstrapMethods
  2. BootstrapMethods中的引导方法会调用java.lang.invoke.LambdaMetafactory#metafactory动态生成新的类和一个静态方法,并且会返回一个CallSite,通过CallSite中的getTarget方法,可以获取MethodHandle,进行新的类和静态方法的调用

参考资料

juejin.cn/post/696683…

zhuanlan.zhihu.com/p/26389041

深入理解JVM字节码