1. 程式人生 > >Java8 Lambda表示式原理掃盲

Java8 Lambda表示式原理掃盲

背景

在使用Lamdba表示式,一直以為是內部類的方式實現的,但是一想如果每次呼叫都例項化一個內部類,效能肯定不好,難道Java裡的lambda表示式真的是這麼實現的嗎?也許是該研究下原理了。

正文

  1. 測試程式碼:
public class Test{
    public void test() {
        Runnable r = () -> System.out.println(123);
        r.run();
    }
}

執行編譯命令javac -g Test.java,得到class檔案。

  1. 檢視位元組碼

檢視位元組碼javap -p -verbose Test

得到:

Classfile /Users/liushijie/learn/Test.class
  Last modified Nov 20, 2018; size 1058 bytes
  MD5 checksum febbe61fdc1f4564d2e039067752d6fc
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#21         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#26         // #0:run:()Ljava/lang/Runnable;
   #3 = InterfaceMethodref #27.#28        // java/lang/Runnable.run:()V
   #4 = Fieldref           #29.#30        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #31.#32        // java/io/PrintStream.println:(I)V
   #6 = Class              #33            // Test
   #7 = Class              #34            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               LTest;
  #15 = Utf8               test
  #16 = Utf8               r
  #17 = Utf8               Ljava/lang/Runnable;
  #18 = Utf8               lambda$test$0
  #19 = Utf8               SourceFile
  #20 = Utf8               Test.java
  #21 = NameAndType        #8:#9          // "<init>":()V
  #22 = Utf8               BootstrapMethods
  #23 = MethodHandle       #6:#35         // 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;
  #24 = MethodType         #9             //  ()V
  #25 = MethodHandle       #6:#36         // invokestatic Test.lambda$test$0:()V
  #26 = NameAndType        #37:#38        // run:()Ljava/lang/Runnable;
  #27 = Class              #39            // java/lang/Runnable
  #28 = NameAndType        #37:#9         // run:()V
  #29 = Class              #40            // java/lang/System
  #30 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #31 = Class              #43            // java/io/PrintStream
  #32 = NameAndType        #44:#45        // println:(I)V
  #33 = Utf8               Test
  #34 = Utf8               java/lang/Object
  #35 = Methodref          #46.#47        // 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;
  #36 = Methodref          #6.#48         // Test.lambda$test$0:()V
  #37 = Utf8               run
  #38 = Utf8               ()Ljava/lang/Runnable;
  #39 = Utf8               java/lang/Runnable
  #40 = Utf8               java/lang/System
  #41 = Utf8               out
  #42 = Utf8               Ljava/io/PrintStream;
  #43 = Utf8               java/io/PrintStream
  #44 = Utf8               println
  #45 = Utf8               (I)V
  #46 = Class              #49            // java/lang/invoke/LambdaMetafactory
  #47 = NameAndType        #50:#54        // 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;
  #48 = NameAndType        #18:#9         // lambda$test$0:()V
  #49 = Utf8               java/lang/invoke/LambdaMetafactory
  #50 = Utf8               metafactory
  #51 = Class              #56            // java/lang/invoke/MethodHandles$Lookup
  #52 = Utf8               Lookup
  #53 = Utf8               InnerClasses
  #54 = 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;
  #55 = Class              #57            // java/lang/invoke/MethodHandles
  #56 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #57 = Utf8               java/lang/invoke/MethodHandles
{
  public Test();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    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 3: 0
        line 4: 6
        line 5: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   LTest;
            6       7     1     r   Ljava/lang/Runnable;

  private static void lambda$test$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        123
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         8: return
      LineNumberTable:
        line 3: 0
}
SourceFile: "Test.java"
InnerClasses:
     public static final #52= #51 of #55; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #23 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:
      #24 ()V
      #25 invokestatic Test.lambda$test$0:()V
      #24 ()V

通過位元組碼檔案我們可以看到,編譯出來的位元組碼檔案中新增一些玩意:

  1. 常量池裡(#2)多了一個以前沒有見過的InvokeDynamic指令
  2. 新增了一個靜態私有方法:private static void lambda$test$0(),裡面的內容正好是lambda表示式裡的程式碼;
  3. 新增了一個BootstrapMethods屬性,內部包含一個動態呼叫點列表,因為測試程式碼只有一個lambda表示式,所以我們只能看到一個呼叫點

在執行時有一個連結(link)過程,在JVM層面呼叫。通過連結操作,呼叫上面3中的呼叫點,呼叫點在動態生成實現了FunctionInterface介面的類,方法中則呼叫2中新增的lambda$test$0方法,生成類之後通過建構函式例項化一個物件,被呼叫點持有,呼叫點有一個字的常量池。在呼叫invokedynamic

指令之前會發生連結過程。下文引自:參考5
裡面也有提到過多執行緒場景,略過不提。

Before the JVM can execute a dynamic call site (an invokedynamic instruction), the call site must first be linked. Linking is accomplished by calling a bootstrap method which is given the static information content of the call site, and which must produce a method handle that gives the behavior of the call site.

總結

通過一些驗證和資料檢索,大概瞭解lambda的原理,是使用指令與動態生成的內部類來完成呼叫,而且正常只會被連結一次。從這個點上來看,對效能是沒有什麼損失的,可以放心的使用。

問題

自己梳理的比較膚淺,沒有深挖最底層的實現。LambdaMetafactory.metafactory動態呼叫點的連結過程比較長,如果有動態呼叫的場景應該是可以參考的。翻到過一篇問答(見參考6),暫時沒找到合適的場景使用,沒有深入下去的動力。

參考

  1. Lambda表示式實現方式
  2. InvokeDynamic指令JSR 292
  3. https://stackoverflow.com/questions/26775676/explicit-use-of-lambdametafactory
  4. https://zhuanlan.zhihu.com/p/27159693
  5. https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/package-summary.html#package.description
  6. https://stackoverflow.com/questions/26775676/explicit-use-of-lambdametafactory