前回、Stream の裏舞台について見てみました。
ついでなので、今回は Lambda 式の裏舞台について見てみましょう。
はじめに
有名な話ではありますが、以下の匿名クラスを含むコードをコンパイルすると、2つのクラスファイルが生成されます。
public class Main { public static void main(String... args) { Logger.getGlobal().info(new Supplier<String>() { @Override public String get() { return "hello"; } }); } }
以下のようなクラスファイルになります。
Main.class Main$1.class
では、lambdaで書いた場合はどうなるでしょう。
public class Main { public static void main(String... args) { Logger.getGlobal().info(() -> "hello"); } }
こちらはクラスファイルが1つになります。
Main.class
つまり、lambda 式で書いた場合と匿名クラスで書いた場合にはコンパイル後のクラスファイルが異なる ということになります
ラムダ式は何処へ?
それぞれのクラスファイルの中身を見れば理由が分かります。
javap
コマンドを使ってクラスファイルの中身をのぞいてみましょう。
$ javap -v Main.class
最初は普通の匿名クラスでコンパイルした方を見てみます。mainメソッドの中身だけ抜粋します。
... 0: invokestatic #2 // Method java/util/logging/Logger.getGlobal:()Ljava/util/logging/Logger; 3: new #3 // class Main$1 6: dup 7: invokespecial #4 // Method Main$1."<init>":()V 10: invokevirtual #5 // Method java/util/logging/Logger.info:(Ljava/util/function/Supplier;)V 13: return ...
普通にMain$1クラスをインスタンス化して info()
を呼んでいるだけです。
(invokespecial
がコンストラクタ呼び出し。invokevirtual
がインスタンスメソッドの呼び出しです。
new
でオブジェクト確保してスタックに積む。dup
でスタックの最上位を複製してスタックに積む。invokespecial
でスタックからPOPしてコンストラクタ呼び出しといった流れになります)
では、ラムダ式版はどうでしょう。
... 0: invokestatic #2 // Method java/util/logging/Logger.getGlobal:()Ljava/util/logging/Logger; 3: invokedynamic #3, 0 // InvokeDynamic #0:get:()Ljava/util/function/Supplier; 8: invokevirtual #4 // Method java/util/logging/Logger.info:(Ljava/util/function/Supplier;)V 11: return ...
先程の匿名クラスのインスタンス化の箇所が、invokedynamic (略して indy と呼ばれます)に変わっています。
さらにインナークラスの項は以下のように定義されており、
InnerClasses: public static final #52= #51 of #55; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
そして、BootstrapMethods の項は以下のようになっています。
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 ()Ljava/lang/Object; #29 invokestatic Main.lambda$main$0:()Ljava/lang/String; #30 ()Ljava/lang/String;
indy は、BootstrapMethods で定義された内容によって呼び出し先を動的に変更します。
BootstrapMethod
indy で呼ばれる java.lang.invoke.LambdaMetafactory.metafactory()
の中身がどうなっているかを見てみましょう。
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(); }
大まかに見ると InnerClassLambdaMetafactory
をインスタンス化して buildCallSite()
していることがわかります。
InnerClassLambdaMetafactory
のインスタンス化部分、コンストラクタの中身を見てみましょう。
長いので大幅に省略すると以下のようになります。
public InnerClassLambdaMetafactory(MethodHandles.Lookup caller, /*...*/) throws LambdaConversionException { super(caller, /*...*/); // ... cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); // ... }
ClassWriter
というクラスに見覚えのある方がいるかと思います。cglib などでも使っている ASM(Java バイトコードを操作するライブラリ)のそれです。jdk.internal
というパッケージの中に、そのまま ASM を取り込んでいます。
import jdk.internal.org.objectweb.asm.*;
では続いて buildCallSite()
の中身の見ておきましょう。といっても長いので雰囲気だけ残して大幅に省略します。
@Override CallSite buildCallSite() throws LambdaConversionException { final Class<?> innerClass = spinInnerClass(); if (invokedType.parameterCount() == 0) { // ... Object inst = ctrs[0].newInstance(); return new ConstantCallSite(MethodHandles.constant(samBase, inst)); } else { UNSAFE.ensureClassInitialized(innerClass); return new ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP .findStatic(innerClass, NAME_FACTORY, invokedType)); } }
今回見たいのは、先頭行にある spinInnerClass()
にあります。
ASM でクラス生成
spinInnerClass()
の中身です。ちょっと長いですが、そのまま載せちゃいます。
private Class<?> spinInnerClass() throws LambdaConversionException { String[] interfaces; String samIntf = samBase.getName().replace('.', '/'); boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase); if (markerInterfaces.length == 0) { interfaces = new String[]{samIntf}; } else { // Assure no duplicate interfaces (ClassFormatError) Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1); itfs.add(samIntf); for (Class<?> markerInterface : markerInterfaces) { itfs.add(markerInterface.getName().replace('.', '/')); accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface); } interfaces = itfs.toArray(new String[itfs.size()]); } cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC, lambdaClassName, null, JAVA_LANG_OBJECT, interfaces); // Generate final fields to be filled in by constructor for (int i = 0; i < argDescs.length; i++) { FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, argNames[i], argDescs[i], null, null); fv.visitEnd(); } generateConstructor(); if (invokedType.parameterCount() != 0) { generateFactory(); } // Forward the SAM method MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName, samMethodType.toMethodDescriptorString(), null, null); mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true); new ForwardingMethodGenerator(mv).generate(samMethodType); // Forward the bridges if (additionalBridges != null) { for (MethodType mt : additionalBridges) { mv = cw.visitMethod(ACC_PUBLIC|ACC_BRIDGE, samMethodName, mt.toMethodDescriptorString(), null, null); mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true); new ForwardingMethodGenerator(mv).generate(mt); } } if (isSerializable) generateSerializationFriendlyMethods(); else if (accidentallySerializable) generateSerializationHostileMethods(); cw.visitEnd(); // Define the generated class in this VM. final byte[] classBytes = cw.toByteArray(); // If requested, dump out to a file for debugging purposes if (dumper != null) { AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { dumper.dumpClass(lambdaClassName, classBytes); return null; } }, null, new FilePermission("<<ALL FILES>>", "read, write"), // createDirectories may need it new PropertyPermission("user.dir", "read")); } return UNSAFE.defineAnonymousClass(targetClass, classBytes, null); }
細かい解説は省略しますが、ASM でランタイム時にクラスを生成し、cw.toByteArray()
で得たバイトコードをUNSAFE.defineAnonymousClass()
でクラス定義しています(defineClass ではなく defineAnonymousClass を使った方がパフォーマンスで6%有利ということで匿名クラス定義にしているようです)(UNSAFE を JDK から無くそうといった提案もありますが、また使われちゃってますね)。
FieldVisitor
や MethodVisitor
などは ASM 使ったことあれば馴染みですよね。
旧来の匿名クラスだと、コンパイルにより生成されるクラスファイルが大量になりすぎ、かつ利用しないものもコンパイルタイムに必要ということで、ランタイム時にクラス生成という結論になったようです。たしか。
なお、これらのクラス生成はオンデマンドで初回のみとなるので、大幅なパフォーマンス劣化などは発生しないはずです。
ASMで生成したラムダ式のクラスを見る
先程の spinInnerClass()
の中には以下のような箇所があります。
// If requested, dump out to a file for debugging purposes if (dumper != null) { // ... }
生成したクラスをダンプできる仕組みですね。
JDK のチケットでいうと以下になります。
システムプロパティで jdk.internal.lambda.dumpProxyClasses=<dir>
と指定してあげればランタイム時に生成したクラスファイルがファイル出力されます。
ダンプして中身を確認してみましょう。
java 起動時に指定するか、ソースの先頭にでも以下のように指定します。
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
今回の例だと以下のようなクラスファイルが生成されるので、javap してみましょう(必要に応じて$はエスケープしてください)。
Main$$Lambda$1.class $ javap -v Main$$Lambda$1.class
以下のような出力となります。短めなので全文載せます。
Classfile Main$$Lambda$1.class Last modified 20XX/XX/XX; size 361 bytes MD5 checksum 1e319f55e80e26bc8b090ea1fd2c8c1d final class Main$$Lambda$1 implements java.util.function.Supplier minor version: 0 major version: 52 flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC Constant pool: #1 = Utf8 Main$$Lambda$1 #2 = Class #1 // Main$$Lambda$1 #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object #5 = Utf8 java/util/function/Supplier #6 = Class #5 // java/util/function/Supplier #7 = Utf8 <init> #8 = Utf8 ()V #9 = NameAndType #7:#8 // "<init>":()V #10 = Methodref #4.#9 // java/lang/Object."<init>":()V #11 = Utf8 get #12 = Utf8 ()Ljava/lang/Object; #13 = Utf8 Ljava/lang/invoke/LambdaForm$Hidden; #14 = Utf8 Main #15 = Class #14 // Main #16 = Utf8 lambda$main$0 #17 = Utf8 ()Ljava/lang/String; #18 = NameAndType #16:#17 // lambda$main$0:()Ljava/lang/String; #19 = Methodref #15.#18 // Main.lambda$main$0:()Ljava/lang/String; #20 = Utf8 Code #21 = Utf8 RuntimeVisibleAnnotations { public java.lang.Object get(); descriptor: ()Ljava/lang/Object; flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: invokestatic #19 // Method Main.lambda$main$0:()Ljava/lang/String; 3: areturn RuntimeVisibleAnnotations: 0: #13() }
0: invokestatic #19
とあり、Main.lambda$main$0()
というstaticメソッドを呼び出していることがわかります。このメソッドは何かと言えば、Main.class
の中にあるプライベートメソッドです。
なので、javap コマンドに -p
オプションを付けてプライベートメソッドを見てみましょう。
$ javap -p -v Main.class
当該箇所を抜き出すと以下のようになっています。
private static java.lang.String lambda$main$0(); descriptor: ()Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=0, args_size=0 0: ldc #5 // String hello 2: areturn LineNumberTable: line 6: 0
ldc
でコンスタントプールから得た hello
という文字をスタックに Push して戻るとなっています。
というわけで、擬似的にコードとして表現すると以下のようになります。
public class Main { // ランタイム時に作成された内部クラス final class Main$$Lambda$1 implements Supplier { @Hidden public Object get() { return Main.lambda$main$0(); } } // コンパイル時に作成された static メソッド private static String lambda$main$0() { return "hello"; } public static void main(String... args) { Logger.getGlobal().info(new Main$$Lambda$1()); } }
これで、消えた lambda 式の行方が分かりました。
まとめ
- lambda 式は、匿名クラスとは異なり、コンパイルによるクラス生成が行われない
- lambda 式は、実行時に indy の仕組みを使って動的にバイトコードを生成する
- 作成されたバイトコードは匿名クラスとしてロードされるため、匿名クラスとして定義したのと同じように動く