[Java] 关于OpenJDK对Java 8 lambda表达式的运行时实现的查看方式

有同学私信我说看到之前一个回答(Java中普通lambda表达式和方法引用本质上有什么区别? - RednaxelaFX的回答 - 知乎)里我给出了OpenJDK对lambda表达式的实现的例子,问说例子里的代码是如何获得的。

简短的答案是:是使用内部调试参数dump出了Class文件,然后人肉反编译Class文件给出了我的回答中的代码。而其实这些步骤只是我基于已有的知识所采用的简略步骤,大家即便不知道实现细节,也可以靠自己一步步探索出来的。

我是如何做的:

首先我知道Oracle JDK 8 / OpenJDK 8对lambda表达式在运行时的实现方式是动态生成跟匿名内部类相似形式的类,而负责生成代码的类位于java.lang.invoke.InnerClassLambdaMetafactory。可以看到,这个类里有一个调试用的Java property可以设置:

jdk.internal.lambda.dumpProxyClasses

相关代码在:

    static {
        final String key = "jdk.internal.lambda.dumpProxyClasses";
        String path = AccessController.doPrivileged(
                new GetPropertyAction(key), null,
                new PropertyPermission(key , "read"));
        dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
    }

所以我们在启动Java的时候,传入这个参数:

-Djdk.internal.lambda.dumpProxyClasses=<path_to_your_dump_directory>

就可以让JDK把lambda表达式对应的运行时生成的类给dump下来了。

接下来我会把这些dump下来的Class文件用javap解读成文本形式,然后人肉将其反编译到Java代码。涉及的字节码其实都非常简单,即便人肉反编译也非常非常快。大家如果还是喜欢用现成的反编译器也大可去用自己喜欢的反编译器。

然而我是如何知道“首先”的那步内容的呢?

大家可以如何着手去调查:

即便没有文档也可以顺藤摸瓜,写个例子慢慢调查。

例如说,随便写个这样的例子:

import java.util.Arrays;

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

然后用javac -g MyTest.java编译,再用javap来把Class文件解读为文本形式:

$ javap -verbose -private MyTest
Classfile /private/tmp/MyTest.class
  Last modified May 28, 2017; size 1260 bytes
  MD5 checksum 2ce5e67938afee50b0dc8841569ea12e
  Compiled from "MyTest.java"
public class 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            // 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               LMyTest;
  #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 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               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         // 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 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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMyTest;

  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 5: 0
        line 6: 7
        line 7: 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 5: 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 MyTest.lambda$main$0:([Ljava/lang/String;)V
      #28 ()V

(Whenever I open this page in Chrome with the macOS Chinese input method active, Chrome crashes immediately. I'll have to write the following in English for now. Hopefully the crash issue goes away soon so that I can rewrite it back to Chinese.)

As shown above, the two lines of source code in main() maps to the following bytecode by javac:

// 1st line:
// Runnable r = () -> System.out.println(Arrays.toString(args));
         0: aload_0
         1: invokedynamic #2,  0              // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
         6: astore_1
// 2nd line:
// r.run();
         7: aload_1
         8: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V

The 2nd line, which invokes the lambda, is the same as a normal Runnable.run() invocation, through invokeinterface. So it doesn't matter whether the Runnable was created using a lambda expression or some other way (e.g. pre-Java 8 anonymous inner classes), the invocation side is always the same.

The interesting part is on the 1st line, where there's a curious-looking invokedynamic instruction involved. Looking at its operands, "#2", is:

   #2 = InvokeDynamic      #0:#30         // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;

which specifies a "BootstrapMethod" (#0) and a "NameAndType" (#30). Let's look at the BootstrapMethod part:

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 MyTest.lambda$main$0:([Ljava/lang/String;)V
      #28 ()V

(Note that the body of the lambda expression is actually desugared into a private static method, lambda$main$0.)

Here we can see that the bootstrap method specified is 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;, from which we can see its use of the InnerClassLambdaMetaFactory:

    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();
    }

That's where we would start following the call and dig in, and then search for keywords like "dump", "trace", "debug", etc, to see if there's any debugging switch/flags embedded in the implementation that would give us more information for debugging.


Have fun debugging!

What Else?

Of course, an even better way to get started is to RTFM. Here's the design documentation of the lambda expression translation strategy: Translation of Lambda Expressions

文章被以下专栏收录
16 条评论
推荐阅读