uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Java8におけるindyとLambdaの絶妙な関係、もしくはSAMタイプを継承する内部クラスの.classファイルはどこへ行ったの?

JJUG CCC 2013 Springのさくらばさんのセッションで「Java 8の怖い話」として、Lambda式をJVM上で実行するにあたり、

  • invokedynamicを使っている
  • Lambda式の仕様は、意味的にはSamタイプを継承する内部クラスのインスタンスのはずだが、lambda式を使っているJavaソースをコンパイルしても、内部クラスに対応する.classファイルが生成されない

という話を聞きました。Lambdaの実行に、内部的にバイトコードを生成して使っている、という話だったのですが、indy使ってるってのは、良く考えると解せないです。Lambdaになんらかの動的な側面があるんでしょうかね???

ということで、調べてみました。

[:contents]

結論

結論から書くと、OracleのJava8のJDKのJavacは、Lambda式の本体を、SAMタイプを継承する内部クラスのメソッドではなく、コンパイル対象のクラスに直接所属するメソッドとしてコンパイルします。また、Lambda式の使用は、invokedynamic命令にコンパイルされます。そして確かに、コンパイル時には、そのメソッドを持つSAMタイプを継承するクラスの.classファイルは生成されません。

そして、invokedynamic命令の初回実行時に、その命令に付随させたブートストラップメソッドの処理の中で、SAMタイプを継承するクラスを動的にオンメモリにバイト列として生成し、それをクラスローダにロードさせ、そのクラスのインスタンスをnewする処理が、invokedynamicの実際の処理に置き換わります*1。この生成されたクラスのSAMメソッドではおそらく、MethodHandle経由で先の(2013/6/13削除。こちらを見ると、MH経由ではない)Lambda本体に相当するメソッドを呼び出します。

ちなみに、「オンメモリにクラスを動的に生成する」という処理を、Java VMではObjectweb asmライブラリを使って行なっています。JVMの中でasmを使ってるとは知らなかった。

indyをLambda式の評価に使ってる理由の一つは、おそらく、lambda式の初回呼び出し時までSAMタイプを継承したクラスの生成およびロードのタイミングを遅延させるためです。この方式なら、例えLambda式が何100個記述されていようとも、そのLambda式が実際に呼び出されなければクラスは全く生成もロードもされないので、クラスロード処理やクラスファイルを読み込むためのファイルアクセス処理が節約でき、起動や初期化の時間が短縮できるのでしょう。また、内部クラスという別クラスではなく、実際にそのクラスにLambda本体のメソッドがあるので、内部クラスを使った場合に生じていた可視性の問題の回避も簡略化できたはずです。

賢いわー。

(2013/06/12 23:52追記)
桜庭さんより(!)Facebookにてコメント頂き、本件に関し、この資料「Lambda: A peek under the hood」が参考になるとのこと。おお!! ざっと見ですが、利点として、字面が同じlambdaは生成SAMクラスを共有できる、とかもあるようです。Type profile pollutionは意味がわかりませんでしたorz。ありがとうございました>桜庭さん

確認してみよう

例えば、Lambda式を含む、

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

こんなJavaソースをまずはコンパイルして実行してみます。

$ javac Test.java
$ java Test
hello
$ ls -la
total 16
drwxr-xr-x   4 uehaj  staff   136  6 12 22:22 ./
drwxr-xr-x  41 uehaj  staff  1394  6 12 22:20 ../
-rw-r--r--   1 uehaj  staff  1095  6 12 22:22 Test.class
-rw-r--r--   1 uehaj  staff   182  6 12 22:21 Test.java

確かに、Test.class以外のクラスファイルは生成されてませんね。javapしてみましょう。

$ javap -p Test
Compiled from "Test.java"
public class Test {
  public Test();
  static void foo(java.lang.Runnable);
  public static void main(java.lang.String[]);
  private static void lambda$0();   // ←★
}

★のところの「lambda$0();」がLamda式のボディに対応するメソッドです。

メソッドの宣言だけではなく、メソッド本体のバイトコードとかコンスタントプール情報も見るために、javapに-c -p -vオプションをつけて実行してみます。以降、行番号は判りやすさのために付与しました。

$ javap -p -c -v Test
     1	Classfile /Users/uehaj/work/201305/java8_2/y/Test.class
     2	  Last modified 2013/06/12; size 1095 bytes
     3	  MD5 checksum d69a213c626bb412c306b8f3ea88cac4
     4	  Compiled from "Test.java"
     5	public class Test
     6	  SourceFile: "Test.java"
     7	  InnerClasses:
     8	       public static final #56= #55 of #59; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
     9	  BootstrapMethods:
    10	    0: #25 invokestatic java/lang/invoke/LambdaMetafactory.metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    11	      Method arguments:
    12	        #26 invokeinterface java/lang/Runnable.run:()V
    13	        #27 invokestatic Test.lambda$0:()V
    14	        #28 ()V
    15	  minor version: 0
    16	  major version: 52
    17	  flags: ACC_PUBLIC, ACC_SUPER
    18	Constant pool:
    19	   #1 = Methodref          #9.#21         //  java/lang/Object."<init>":()V
    20	   #2 = InterfaceMethodref #22.#23        //  java/lang/Runnable.run:()V
    21	   #3 = InvokeDynamic      #0:#29         //  #0:lambda$:()Ljava/lang/Runnable;
    22	   #4 = Methodref          #8.#30         //  Test.foo:(Ljava/lang/Runnable;)V
    23	   #5 = Fieldref           #31.#32        //  java/lang/System.out:Ljava/io/PrintStream;
    24	   #6 = String             #33            //  hello
    25	   #7 = Methodref          #34.#35        //  java/io/PrintStream.println:(Ljava/lang/String;)V
    26	   #8 = Class              #36            //  Test
    27	   #9 = Class              #37            //  java/lang/Object
    28	  #10 = Utf8               <init>
    29	  #11 = Utf8               ()V
    30	  #12 = Utf8               Code
    31	  #13 = Utf8               LineNumberTable
    32	  #14 = Utf8               foo
    33	  #15 = Utf8               (Ljava/lang/Runnable;)V
    34	  #16 = Utf8               main
    35	  #17 = Utf8               ([Ljava/lang/String;)V
    36	  #18 = Utf8               lambda$0
    37	  #19 = Utf8               SourceFile
    38	  #20 = Utf8               Test.java
    39	  #21 = NameAndType        #10:#11        //  "<init>":()V
    40	  #22 = Class              #38            //  java/lang/Runnable
    41	  #23 = NameAndType        #39:#11        //  run:()V
    42	  #24 = Utf8               BootstrapMethods
    43	  #25 = MethodHandle       #6:#40         //  invokestatic java/lang/invoke/LambdaMetafactory.metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    44	  #26 = MethodHandle       #9:#2          //  invokeinterface java/lang/Runnable.run:()V
    45	  #27 = MethodHandle       #6:#41         //  invokestatic Test.lambda$0:()V
    46	  #28 = MethodType         #11            //  ()V
    47	  #29 = NameAndType        #42:#43        //  lambda$:()Ljava/lang/Runnable;
    48	  #30 = NameAndType        #14:#15        //  foo:(Ljava/lang/Runnable;)V
    49	  #31 = Class              #44            //  java/lang/System
    50	  #32 = NameAndType        #45:#46        //  out:Ljava/io/PrintStream;
    51	  #33 = Utf8               hello
    52	  #34 = Class              #47            //  java/io/PrintStream
    53	  #35 = NameAndType        #48:#49        //  println:(Ljava/lang/String;)V
    54	  #36 = Utf8               Test
    55	  #37 = Utf8               java/lang/Object
    56	  #38 = Utf8               java/lang/Runnable
    57	  #39 = Utf8               run
    58	  #40 = Methodref          #50.#51        //  java/lang/invoke/LambdaMetafactory.metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    59	  #41 = Methodref          #8.#52         //  Test.lambda$0:()V
    60	  #42 = Utf8               lambda$
    61	  #43 = Utf8               ()Ljava/lang/Runnable;
    62	  #44 = Utf8               java/lang/System
    63	  #45 = Utf8               out
    64	  #46 = Utf8               Ljava/io/PrintStream;
    65	  #47 = Utf8               java/io/PrintStream
    66	  #48 = Utf8               println
    67	  #49 = Utf8               (Ljava/lang/String;)V
    68	  #50 = Class              #53            //  java/lang/invoke/LambdaMetafactory
    69	  #51 = NameAndType        #54:#58        //  metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    70	  #52 = NameAndType        #18:#11        //  lambda$0:()V
    71	  #53 = Utf8               java/lang/invoke/LambdaMetafactory
    72	  #54 = Utf8               metaFactory
    73	  #55 = Class              #60            //  java/lang/invoke/MethodHandles$Lookup
    74	  #56 = Utf8               Lookup
    75	  #57 = Utf8               InnerClasses
    76	  #58 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    77	  #59 = Class              #61            //  java/lang/invoke/MethodHandles
    78	  #60 = Utf8               java/lang/invoke/MethodHandles$Lookup
    79	  #61 = Utf8               java/lang/invoke/MethodHandles
    80	{
    81	  public Test();
    82	    flags: ACC_PUBLIC
    83	    Code:
    84	      stack=1, locals=1, args_size=1
    85	         0: aload_0       
    86	         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
    87	         4: return        
    88	      LineNumberTable:
    89	        line 3: 0
    90	
    91	  static void foo(java.lang.Runnable);
    92	    flags: ACC_STATIC
    93	    Code:
    94	      stack=1, locals=1, args_size=1
    95	         0: aload_0       
    96	         1: invokeinterface #2,  1            // InterfaceMethod java/lang/Runnable.run:()V
    97	         6: return        
    98	      LineNumberTable:
    99	        line 5: 0
   100	        line 6: 6
   101	
   102	  public static void main(java.lang.String[]);
   103	    flags: ACC_PUBLIC, ACC_STATIC
   104	    Code:
   105	      stack=1, locals=1, args_size=1
   106	         0: invokedynamic #3,  0              // InvokeDynamic #0:lambda$:()Ljava/lang/Runnable;
   107	         5: invokestatic  #4                  // Method foo:(Ljava/lang/Runnable;)V
   108	         8: return        
   109	      LineNumberTable:
   110	        line 8: 5
   111	        line 9: 8
   112	
   113	  private static void lambda$0();
   114	    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
   115	    Code:
   116	      stack=2, locals=0, args_size=0
   117	         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
   118	         3: ldc           #6                  // String hello
   119	         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
   120	         8: return        
   121	      LineNumberTable:
   122	        line 8: 0
   123	}

長いっ!

なので以降、分解して見ていきます。

Lambda式はどうなったか

Lambdaの本体に対応しそうなのは、

   113	  private static void lambda$0();
   114	    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
   115	    Code:
   116	      stack=2, locals=0, args_size=0
   117	         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
   118	         3: ldc           #6                  // String hello
   119	         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
   120	         8: return        
   121	      LineNumberTable:
   122	        line 8: 0
   123	}

の部分であり、確かにlambda$0メソッドが、

()->{ System.out.println("hello"); }

の「System.out.println("hello")」近辺に対応していそうです。

invokedynamicの動き

まず、mainメソッドに対応する以下の部分を見ると、

   102	  public static void main(java.lang.String[]);
   103	    flags: ACC_PUBLIC, ACC_STATIC
   104	    Code:
   105	      stack=1, locals=1, args_size=1
   106	         0: invokedynamic #3,  0              // InvokeDynamic #0:lambda$:()Ljava/lang/Runnable;
   107	         5: invokestatic  #4                  // Method foo:(Ljava/lang/Runnable;)V
   108	         8: return        
   109	      LineNumberTable:
   110	        line 8: 5
   111	        line 9: 8

確かにinvokedynamic命令が生成されていることがわかります。このinvokedynamic命令に対応する「ブートストラップメソッド」は、javap出力の冒頭にある

     9	  BootstrapMethods:
    10	    0: #25 invokestatic java/lang/invoke/LambdaMetafactory.metaFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    11	      Method arguments:
    12	        #26 invokeinterface java/lang/Runnable.run:()V
    13	        #27 invokestatic Test.lambda$0:()V
    14	        #28 ()V

ここのところですね。ブートストラップメソッドが表している実体は、静的メソッドへのポインタであり、ここでは具体的には、JDKの提供するAPIであるメソッドjava.lang.invoke.LambdaMetafactory.metaFactory()です。

LambdaMetaFactory

ブートストラップメソッドとして仕掛けられているLambdaMetaFactoryは、このケースでは最終的にjava.lang.invoke.InnerClassLambdaMetafactory.javaのspinInnerClass()あたりの以下の処理を呼び出します。

(略)
import jdk.internal.org.objectweb.asm.*;
import static jdk.internal.org.objectweb.asm.Opcodes.*;

(略)
    private final ClassWriter cw;                    // ASM class writer
(略)

    private Class<?> spinInnerClass() throws LambdaConversionException {
(略)    
        cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
                 lambdaClassName, null,
                 NAME_MAGIC_ACCESSOR_IMPL, interfaces);

        // Generate final fields to be filled in by constructor
        for (int i = 0; i < argTypes.length; i++) {
            FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, argNames[i], argTypes[i].getDescriptor(),
                                            null, null);
            fv.visitEnd();
        }

        generateConstructor();

        MethodAnalyzer ma = new MethodAnalyzer();

        // Forward the SAM method
        if (ma.getSamMethod() == null) {
            throw new LambdaConversionException(String.format("Functional interface method not found: %s", samMethodType));
        } else {
            generateForwardingMethod(ma.getSamMethod(), false);
        }

        // Forward the bridges
        // @@@ The commented-out code is temporary, pending the VM's ability to bridge all methods on request
        // @@@ Once the VM can do fail-over, uncomment the !ma.wasDefaultMethodFound() test, and emit the appropriate
        // @@@ classfile attribute to request custom bridging.  See 8002092.
        if (!ma.getMethodsToBridge().isEmpty() /* && !ma.conflictFoundBetweenDefaultAndBridge() */ ) {
            for (Method m : ma.getMethodsToBridge()) {
                generateForwardingMethod(m, true);
            }
        }

        if (isSerializable) {
            generateWriteReplace();
        }

        cw.visitEnd();

        // Define the generated class in this VM.

        final byte[] classBytes = cw.toByteArray();

        ClassLoader loader = targetClass.getClassLoader(); // ★★
(略)
        return (Class<?>) Unsafe.getUnsafe().defineClass(lambdaClassName, classBytes, 0, classBytes.length,
                                                                   loader, pd);
    }

asmのClassWriterで生成したバイト列classBytesを、★★以降でクラスローダでロードしているようです。
どういうクラスを生成しているか、までは実は追ってないのですが、MethodHandle経由で渡してきたlambda$0をinvokeするようなコードを含むメソッドを含むクラスが生成されるのではないかと思います(もしくはlambda$0のバイトコード本体をコピーする??まさかね)。(2013/6/13削除)
こちらを見ると、直接lambda$0メソッドをinvokevirtual/invokestaticで呼んでいるようです(2013/6/13追記)。

まとめ

APIリファレンスが日本語に訳されたので、invokedynamicあたりのAPI(MethodHandle, CallSite,....)を初めて読む気になりました(へたれ)。ありがとうオラクルの人(寺田さん?)。

invokedynamicは今さらながらおもしろいな。今、Brainfuckindyで実装してみるテスト中です。

*1:この置き換えはinvokedynamicの通常動作です。CallSiteのインストールの話ね。