直面底层之字节码看匿名内部类和lamda

1,579 阅读4分钟

前言

我们日常开发中的通常会遇到匿名内部类,并且匿名内部类会持有外部类的引用,那么字节码层面是如何的呢?本文从字节码层面看内部类和 lamda


一、匿名内部类

public class NoNameInnerClass {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {

            }
        };
        r.run();
    }
}

我们new 了一个 runable 然后调用 run方法,现在我们通过javac 将上述类编译成class 文件,我们看到编译后会生成 NoNameInnerClass$1.class 和 NoNameInnerClass.class 两个class文件。

我们先看下NoNameInnerClass$1.class 和 NoNameInnerClass.class

//NoNameInnerClass$1.class
final class NoNameInnerClass$1 implements Runnable {
    NoNameInnerClass$1() {
    }

    public void run() {
    }
}
//NoNameInnerClass.class
public class NoNameInnerClass {
    public NoNameInnerClass() {
    }

    public static void main(String[] var0) {
        Runnable var1 = new Runnable() {
            public void run() {
            }
        };
        var1.run();
    }
}

直观上看 虽然多生成了一个 NoNameInnerClass$1,但NoNameInnerClass 中并没有用到,别着急我们看下 NoNameInnerClass 的字节码

public class javaplan.NoNameInnerClass {
  //NoNameInnerClass 的默认构造方法
  public javaplan.NoNameInnerClass();
    Code:
       0: aload_0
       1: invokespecial #1   // Method java/lang/Object."<init>":()V
       4: return
 //main方法
  public static void main(java.lang.String[]);
    Code:
       //new 了一个 NoNameInnerClass$1 然后调用了NoNameInnerClass$1 构造方法
       0: new           #2   // class javaplan/NoNameInnerClass$1
       3: dup
       4: invokespecial #3  // Method javaplan/NoNameInnerClass$1."<init>":()V
       7: astore_1
       //调用 NoNameInnerClass$1 run 方法
       8: aload_1
       9: invokeinterface #4,  1  // InterfaceMethod java/lang/Runnable.run:()V
      14: return
}

可以看到,在字节码层面,其实生成了一个 NoNameInnerClass$1 的实例对象,并调用了run方法,不然哪会无缘无故的多生成一个class 文件呢?


二、lamda

同上述步骤,我们写一个简单的lamda

public class LamdaTest {
    public static void main(String[] args) {
        Runnable r = ()->{

        };
        r.run();
    }

}

然后用javac 编译生成 class 文件,奇怪的是我们发现并没有生成多余的class 文件,只有一个 LamdaTest.class

我们看下 LamdaTest.class 的字节码注意这里简单用 javap -c 的话是看不出什么门道的,需要用 javap -c -v -p :

public class javaplan.LamdaTest
  //
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#15         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#20         // #0:run:()Ljava/lang/Runnable;
   #3 = InterfaceMethodref #21.#22        // java/lang/Runnable.run:()V
   #4 = Class              #23            // javaplan/LamdaTest
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               lambda$main$0
  #13 = Utf8               SourceFile
  #14 = Utf8               LamdaTest.java
  #15 = NameAndType        #6:#7          // "<init>":()V
  #16 = Utf8               BootstrapMethods
  #17 = MethodHandle       #6:#25         // 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;
  #18 = MethodType         #7             //  ()V
  #19 = MethodHandle       #6:#26         // invokestatic javaplan/LamdaTest.lambda$main$0:()V
  #20 = NameAndType        #27:#28        // run:()Ljava/lang/Runnable;
  #21 = Class              #29            // java/lang/Runnable
  #22 = NameAndType        #27:#7         // run:()V
  #23 = Utf8               javaplan/LamdaTest
  #24 = Utf8               java/lang/Object
  #25 = Methodref          #30.#31        // 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;
  #26 = Methodref          #4.#32         // javaplan/LamdaTest.lambda$main$0:()V
  #27 = Utf8               run
  #28 = Utf8               ()Ljava/lang/Runnable;
  #29 = Utf8               java/lang/Runnable
  #30 = Class              #33            // java/lang/invoke/LambdaMetafactory
  #31 = NameAndType        #34:#38        // 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;
  #32 = NameAndType        #12:#7         // lambda$main$0:()V
  #33 = Utf8               java/lang/invoke/LambdaMetafactory
  #34 = Utf8               metafactory
  #35 = Class              #40            // java/lang/invoke/MethodHandles$Lookup
  #36 = Utf8               Lookup
  #37 = Utf8               InnerClasses
  #38 = 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;
  #39 = Class              #41            // java/lang/invoke/MethodHandles
  #40 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #41 = Utf8               java/lang/invoke/MethodHandles
{
  public javaplan.LamdaTest();
    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 3: 0

  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 5: 0
        line 8: 6
        line 9: 12

  private static void lambda$main$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 7: 0
}
SourceFile: "LamdaTest.java"
InnerClasses:
     public static final #36= #35 of #39; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #17 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:
      #18 ()V
      #19 invokestatic javaplan/LamdaTest.lambda$main$0:()V
      #18 ()V

可以看到调用了 invokedynamic 指令(专门为lamda 而生),会指向常量池#2;

而常量池 #2 指向了 #0 ,#0 对应的是一个特殊查找即BootstrapMethods:

BootstrapMethods: 首先用invokestatic 调用了 metafactory 方法

入参:

看下这个方法实现:

  • caller:JVM 提供的查找上下文
  • invokedName:表示调用函数名,在本例中 invokedName 为 "run"
  • samMethodType:函数式接口定义的方法签名(参数类型和返回值类型),本例中为 run 方法的签名 "()void"
  • implMethod:编译时生成的 lambda 表达式对应的静态方法invokestatic LamdaTest.lambda$main$0
  • instantiatedMethodType:一般和 samMethodType 是一样或是它的一个特例,在本例中是 "()void"
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
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();
    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;
    }
}
  • lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时生成一个对应的引导方法(Bootstrap Method)
  • 第一次执行 invokedynamic 指令时,会调用对应的引导方法(Bootstrap Method),该引导方法会调用 LambdaMetafactory.metafactory 方法动态生成内部类
  • 引导方法会返回一个动态调用 CallSite 对象,这个 CallSite 调用实现了 Runnable 接口的内部类
  • lambda 表达式中的内容会被编译成静态方法,前面动态生成的内部类会直接调用该静态方法
  • 执行 lambda 其实是 invokeinterface 调用了 CallSite 生成的内部类的方法

三、小结

1.匿名内部类是在编译期间生成新的 class 文件来实现的
2.lambda 表达式采用的方式是不在编译期间就生成类,而是运行时动态生成一个内部类