Java lambda实现原理解析

2022-10-25  本文已影响0人  allanYan

概述

Java8引入了lambda表达式,那么底层是如何实现的呢?是否是采用匿名内部类实现的呢?

代码样例

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

字节码如下(javap -c -s -l -verbose -private lamb/MyTest.class):

Classfile /Users/xmly/Works/example/target/classes/lamb/MyTest.class
  Last modified 2022-10-24; size 1270 bytes
  MD5 checksum 308b472813d9700f4dca0763aeeb505a
  Compiled from "MyTest.java"
public class lamb.MyTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#25         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#30         // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
   #3 = InterfaceMethodref #31.#32        // java/lang/Runnable.run:()V
   #4 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #35.#36        // java/util/Arrays.toString:([Ljava/lang/Object;)Ljava/lang/String;
   #6 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #39            // lamb/MyTest
   #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               Llamb/MyTest;
  #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               lambda$main$0
  #23 = Utf8               SourceFile
  #24 = Utf8               MyTest.java
  #25 = NameAndType        #9:#10         // "<init>":()V
  #26 = Utf8               BootstrapMethods
  #27 = 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;
  #28 = MethodType         #10            //  ()V
  #29 = MethodHandle       #6:#42         // invokestatic lamb/MyTest.lambda$main$0:([Ljava/lang/String;)V
  #30 = NameAndType        #43:#44        // run:([Ljava/lang/String;)Ljava/lang/Runnable;
  #31 = Class              #45            // java/lang/Runnable
  #32 = NameAndType        #43:#10        // run:()V
  #33 = Class              #46            // java/lang/System
  #34 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
  #35 = Class              #49            // java/util/Arrays
  #36 = NameAndType        #50:#51        // toString:([Ljava/lang/Object;)Ljava/lang/String;
  #37 = Class              #52            // java/io/PrintStream
  #38 = NameAndType        #53:#54        // println:(Ljava/lang/String;)V
  #39 = Utf8               lamb/MyTest
  #40 = Utf8               java/lang/Object
  #41 = Methodref          #55.#56        // 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.#57         // lamb/MyTest.lambda$main$0:([Ljava/lang/String;)V
  #43 = Utf8               run
  #44 = Utf8               ([Ljava/lang/String;)Ljava/lang/Runnable;
  #45 = Utf8               java/lang/Runnable
  #46 = Utf8               java/lang/System
  #47 = Utf8               out
  #48 = Utf8               Ljava/io/PrintStream;
  #49 = Utf8               java/util/Arrays
  #50 = Utf8               toString
  #51 = Utf8               ([Ljava/lang/Object;)Ljava/lang/String;
  #52 = Utf8               java/io/PrintStream
  #53 = Utf8               println
  #54 = Utf8               (Ljava/lang/String;)V
  #55 = Class              #58            // java/lang/invoke/LambdaMetafactory
  #56 = NameAndType        #59:#63        // 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;
  #57 = NameAndType        #22:#17        // lambda$main$0:([Ljava/lang/String;)V
  #58 = Utf8               java/lang/invoke/LambdaMetafactory
  #59 = Utf8               metafactory
  #60 = Class              #65            // java/lang/invoke/MethodHandles$Lookup
  #61 = Utf8               Lookup
  #62 = Utf8               InnerClasses
  #63 = 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;
  #64 = Class              #66            // java/lang/invoke/MethodHandles
  #65 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #66 = Utf8               java/lang/invoke/MethodHandles
{
  public lamb.MyTest();
    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 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Llamb/MyTest;

  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: aload_0
         1: invokedynamic #2,  0              // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
         6: astore_1
         7: aload_1
         8: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
        13: return
      LineNumberTable:
        line 7: 0
        line 8: 7
        line 9: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            7       7     1     r   Ljava/lang/Runnable;

  private static void lambda$main$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     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokestatic  #5                  // Method java/util/Arrays.toString:([Ljava/lang/Object;)Ljava/lang/String;
         7: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
}
SourceFile: "MyTest.java"
InnerClasses:
     public static final #61= #60 of #64; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #27 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:
      #28 ()V
      #29 invokestatic lamb/MyTest.lambda$main$0:([Ljava/lang/String;)V
      #28 ()V

分析

可以看到编译后的字节码中出现了几个新的内容,lambda表达式编译后变成了

invokedynamic #2,  0  //0 是预留参数,暂时没有作用
#2 = InvokeDynamic      #0:#30 
#30 = NameAndType        #43:#44  
#43 = Utf8               run
#44 = Utf8               ([Ljava/lang/String;)Ljava/lang/Runnable;

其中0为表示在 [Bootstrap methods表]中的索引:
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;

javac编译

可以看到编译后的字节码里面多了BSM相关的信息,说明javac编译对lambda做了特殊处理:

javac命令的代码调用路径为:
com.sun.tools.javac.Main->com.sun.tools.javac.main.Main->com.sun.tools.javac.main.JavaCompiler.compile2()->com.sun.tools.javac.main.JavaCompiler.desugar()

if (this.source.allowLambda() && scanner.hasLambdas) {
    if (this.shouldStop(CompileState.UNLAMBDA)) {
        return;
    }

    env.tree = LambdaToMethod.instance(this.context).translateTopLevelClass(env, env.tree, localMake);
    this.compileStates.put(env, CompileState.UNLAMBDA);
}
截屏2022-10-25 09.16.11.png

处理BSM相关的代码如下:

private JCExpression makeMetafactoryIndyCall(LambdaToMethod.LambdaAnalyzerPreprocessor.TranslationContext<?> context, int refKind, Symbol refSym, List<JCExpression> indy_args) {
        JCFunctionalExpression tree = context.tree;
        MethodSymbol samSym = (MethodSymbol)this.types.findDescriptorSymbol(tree.type.tsym);
        List<Object> staticArgs = List.of(this.typeToMethodType(samSym.type), new MethodHandle(refKind, refSym, this.types), this.typeToMethodType(tree.getDescriptorType(this.types)));
        ListBuffer<Type> indy_args_types = new ListBuffer();
        Iterator var9 = indy_args.iterator();

        while(var9.hasNext()) {
            JCExpression arg = (JCExpression)var9.next();
            indy_args_types.append(arg.type);
        }

        MethodType indyType = new MethodType(indy_args_types.toList(), tree.type, List.nil(), this.syms.methodClass);
        Name metafactoryName = context.needsAltMetafactory() ? this.names.altMetafactory : this.names.metafactory;
        if (context.needsAltMetafactory()) {
            ListBuffer<Object> markers = new ListBuffer();
            Iterator var12 = tree.targets.tail.iterator();

            while(var12.hasNext()) {
                Type t = (Type)var12.next();
                if (t.tsym != this.syms.serializableType.tsym) {
                    markers.append(t.tsym);
                }
            }

            int flags = context.isSerializable() ? 1 : 0;
            boolean hasMarkers = markers.nonEmpty();
            boolean hasBridges = context.bridges.nonEmpty();
            if (hasMarkers) {
                flags |= 2;
            }

            if (hasBridges) {
                flags |= 4;
            }

            staticArgs = staticArgs.append(flags);
            if (hasMarkers) {
                staticArgs = staticArgs.append(markers.length());
                staticArgs = staticArgs.appendList(markers.toList());
            }

            if (hasBridges) {
                staticArgs = staticArgs.append(context.bridges.length() - 1);
                Iterator var15 = context.bridges.iterator();

                while(var15.hasNext()) {
                    Symbol s = (Symbol)var15.next();
                    Type s_erasure = s.erasure(this.types);
                    if (!this.types.isSameType(s_erasure, samSym.erasure(this.types))) {
                        staticArgs = staticArgs.append(s.erasure(this.types));
                    }
                }
            }

            if (context.isSerializable()) {
                int prevPos = this.make.pos;

                try {
                    this.make.at(this.kInfo.clazz);
                    this.addDeserializationCase(refKind, refSym, tree.type, samSym, tree, staticArgs, indyType);
                } finally {
                    this.make.at(prevPos);
                }
            }
        }

        return this.makeIndyCall(tree, this.syms.lambdaMetafactory, metafactoryName, staticArgs, indyType, indy_args, samSym.name);
    }

Name metafactoryName = context.needsAltMetafactory() ? this.names.altMetafactory : this.names.metafactory;可以知道lambda的BSM有两种:

  1. java.lang.invoke.LambdaMetafactory.metafactory
  2. java.lang.invoke.LambdaMetafactory.altMetafactory

调用链观察

从上面的分析,可以知道lambda最终会调用LambdaMetafactory.metafactory,通过打断点观察,其传入参数如下:

截屏2022-10-25 09.45.50.png

LambdaMetafactory.metafactory最终是调用InnerClassLambdaMetafactory.buildCallSite,代码如下:

       final Class<?> innerClass = spinInnerClass();
        if (invokedType.parameterCount() == 0) {
            final Constructor<?>[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction<Constructor<?>[]>() {
                @Override
                public Constructor<?>[] run() {
                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                    if (ctrs.length == 1) {
                        // The lambda implementing inner class constructor is private, set
                        // it accessible (by us) before creating the constant sole instance
                        ctrs[0].setAccessible(true);
                    }
                    return ctrs;
                }
                    });
            if (ctrs.length != 1) {
                throw new LambdaConversionException("Expected one lambda constructor for "
                        + innerClass.getCanonicalName() + ", got " + ctrs.length);
            }

            try {
                Object inst = ctrs[0].newInstance();
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception instantiating lambda object", e);
            }
        } else {
            try {
                UNSAFE.ensureClassInitialized(innerClass);
                return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception finding constructor", e);
            }
        }

spinInnerClass方法会使用unsafe.defineAnonymousClass来生成VM匿名内部类:

package lamb;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class MyTest$$Lambda$1 implements Runnable {
    private final String[] arg$1;

    private MyTest$$Lambda$1(String[] var1) {
        this.arg$1 = var1;
    }

    private static Runnable get$Lambda(String[] var0) {
        return new MyTest$$Lambda$1(var0);
    }

    @Hidden
    public void run() {
        MyTest.lambda$main$0(this.arg$1);
    }
}

可以看到实际上是一个实现了Runnable接口的匿名类,最终会调用MyTest.lambdamain0方法;

打印匿名类内容需要增加配置:-Djdk.internal.lambda.dumpProxyClasses=

VM anonymous class与匿名内部类

具体介绍可以参考这篇文章:https://www.zhihu.com/question/51132462/answer/124751186

上一篇下一篇

猜你喜欢

热点阅读