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,....)を初めて読む気になりました(へたれ)。ありがとうオラクルの人(寺田さん?)。
*1:この置き換えはinvokedynamicの通常動作です。CallSiteのインストールの話ね。