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;
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
20 #2 = InterfaceMethodref #22.#23
21 #3 = InvokeDynamic #0:#29
22 #4 = Methodref #8.#30
23 #5 = Fieldref #31.#32
24 #6 = String #33
25 #7 = Methodref #34.#35
26 #8 = Class #36
27 #9 = Class #37
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
40 #22 = Class #38
41 #23 = NameAndType #39:#11
42 #24 = Utf8 BootstrapMethods
43 #25 = MethodHandle #6:#40
44 #26 = MethodHandle #9:#2
45 #27 = MethodHandle #6:#41
46 #28 = MethodType #11
47 #29 = NameAndType #42:#43
48 #30 = NameAndType #14:#15
49 #31 = Class #44
50 #32 = NameAndType #45:#46
51 #33 = Utf8 hello
52 #34 = Class #47
53 #35 = NameAndType #48:#49
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
59 #41 = Methodref #8.#52
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
69 #51 = NameAndType #54:#58
70 #52 = NameAndType #18:#11
71 #53 = Utf8 java/lang/invoke/LambdaMetafactory
72 #54 = Utf8 metaFactory
73 #55 = Class #60
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
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
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
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
107 5: invokestatic #4
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
118 3: ldc #6
119 5: invokevirtual #7
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
118 3: ldc #6
119 5: invokevirtual #7
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
107 5: invokestatic #4
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;
(略)
private Class<?> spinInnerClass() throws LambdaConversionException {
(略)
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
lambdaClassName, null,
NAME_MAGIC_ACCESSOR_IMPL, interfaces);
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();
if (ma.getSamMethod() == null) {
throw new LambdaConversionException(String.format("Functional interface method not found: %s", samMethodType));
} else {
generateForwardingMethod(ma.getSamMethod(), false);
}
if (!ma.getMethodsToBridge().isEmpty() ) {
for (Method m : ma.getMethodsToBridge()) {
generateForwardingMethod(m, true);
}
}
if (isSerializable) {
generateWriteReplace();
}
cw.visitEnd();
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追記)。