Java
JVM

invokedynamic調査メモ

More than 1 year has passed since last update.

さんざん色々なところで紹介されているので今更感はありますが、Java SE 7で導入されたinvokedynamicの(自分向け)メモ。


invokedynamicとは

Java Virtual Machine(JVM)の命令で、これを使うと、メソッド呼び出し元と呼び出し先のリンク処理を、アプリケーション側でカスタマイズできます。(invokedynamic以外のメソッド呼び出し用JVM命令(invokeinterface、invokespecial、invokestatic、invokevirtual)の場合は、リンク処理はJVM側で定義されていた)


従来の命令の場合

例えば、以下のJavaプログラムは、javacを使うと以下のバイトコードにコンパイルされます。


A.java

class A {

public static void main(String[] args) {
String name = "Taro";
A a = new A();
a.say(name);
}

public void say(String name) {
System.out.println("Hello, " + name);
}
}


$ javac A.java 

$ java A
Hello, Taro

$ javap -p -v -c A

--- snip ---

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String Taro
2: astore_1
3: new #3 // class A
6: dup
7: invokespecial #4 // Method "<init>":()V
10: astore_2
11: aload_2
12: aload_1
13: invokevirtual #5 // Method say:(Ljava/lang/String;)V
16: return


  • 10番目: astore_2で、スタック上のAオブジェクトをポップして、ローカル変数2に代入。

  • 11番目: aload_2で、ローカル変数2のAオブジェクトをスタックにプッシュ。

  • 12番目: aload_1で、ローカル変数1の"Taro"オブジェクトをスタックにプッシュ。

  • 13番目: invokevirtualで、スタック上のAオブジェクトに対して、スタック上の"Taro"オブジェクトを引数に渡し、say()メソッドを呼び出す。

という流れになります。

invokevirtualは、インスタンスメソッド実行時に使われるJVM命令です。この時の処理の流れは以下の通りです。

[呼び出し元]             [呼び出し先]

A.main(String[]) ---> A#say(String)


invokedynamicの場合

次に、以下のプログラムを考えます。このプログラムをコンパイルすると、invokedynamicを使ったバイトコードが生成されます。

(実はjava.util.functionを使うのはあまりいい例ではありませんが、これくらいしかjavacでinvokedynamicが生成されるの知らなかったため)


B.java

import java.util.function.*;

class B {
public static void main(String[] args) {
String name = "Taro";
Consumer f = a -> System.out.println("Hello, " + a);
f.accept("Taro");
}
}


$ javac B.java 

注意:B.javaの操作は、未チェックまたは安全ではありません。
注意:詳細は、-Xlint:uncheckedオプションを指定して再コンパイルしてください。

$ java B
Hello, Taro

$ javap -p -v -c B

--- snip ---

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String Taro
2: astore_1
3: invokedynamic #3, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
8: astore_2
9: aload_2
10: aload_1
11: invokeinterface #4, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
16: return


  • 3番目のinvokedynamicで、謎メソッド(1)を呼び出す。

  • 8番目のastore_2で、invokedynamicの結果の謎オブジェクト(2)をローカル変数2に代入。

  • 9番目のaload_2で、謎オブジェクト(2)をスタックにプッシュ

  • 10番目のaload_1で、"Taro"オブジェクトをスタックにプッシュ

  • 11番目のinvokeinterfaceで、謎オブジェクト(2)に対して、"Taro"オブジェクトを引数に渡し、accept()メソッドを呼び出す。

という流れになります。

ここで、謎メソッド(1)がbootstrap methodと呼ばれる特殊なメソッドです。java.util.functionを使うとJavaの標準APIのものが使われますが、アプリで指定することもできます。bootstrap methodはjavapで解析すると以下のようにでます。

$ javap -p -v -c B

--- snip ---

BootstrapMethods:
0: #28 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:
#29 (Ljava/lang/Object;)V
#30 invokestatic B.lambda$main$0:(Ljava/lang/Object;)V
#29 (Ljava/lang/Object;)V

この場合は、bootstrap methodとして、java.lang.invoke.LambdaMetafactory.metafactory()が使われるようです。

bootstrap methodは、引数に呼び出し先メソッドの情報が与えられ、呼び出し先メソッドを決定して返します(これを「リンク処理」と言っている)。

処理の流れを簡単に図示すると以下のようになります。JVMは、invokedynamicが実行されると、bootstrap methodを呼び出します。bootstrap methodは、call siteを返します。call siteは、メソッド呼び出し先を指し示すもので、今回の場合はB.lambda$main$0(java.lang.Object)を指し示すcall siteが返ります。JVMは、返ったcall siteを見て、メソッド呼び出し処理を行います。同じinvokedynamicを再び実行する際は、bootstrap methodは呼び出さず、前回と同じメソッドの呼び出し処理を行います(リンクされたメソッドを後から変更する方法もあるみたい)。

【初回のinvokedynamic実行】

[呼び出し元] [呼び出し先]
B.main() --+ +-------> B.lambda$main$0(java.lang.Object) (ラムダ式の処理を実装したメソッド)
| |
V |
LambdaMetafactory.metafactory()
[bootstrap method]

【二回目以降のinvokedynamic実行】
[呼び出し元] [呼び出し先]
B.main() -------------> B.lambda$main$0(java.lang.Object) (ラムダ式の処理を実装したメソッド)


invokedynamicを使ってみる

完全なソースコードはこちら。バイトコード生成にはASMを使っています(ズルして、JDK内部に取り込まれば非公開のASMを使ってます)。

以下はinvokedynamicを使ったバイトコードを生成する処理です。このバイトコードは、callInvokeDynamic(Object, Object)メソッドを含んでいます。callInvokeDynamic()メソッドの中では、引数のオブジェクト2つをスタックにプッシュし、invokedynamicを実行して、返った結果をreturnします。


MyClassFactory.java

  private byte[] createTestInvokeDynamic() {

String className = "TestInvokeDynamic";
String methodName = "callInvokeDynamic";
ClassWriter cw = new ClassWriter(COMPUTE_MAXS);
MethodVisitor mv;
MethodType bootstrapMethodType = MethodType.methodType(
CallSite.class,
MethodHandles.Lookup.class,
String.class,
MethodType.class);
Handle bootstrapMethodHandle = new Handle(
H_INVOKESTATIC,
MyMethodFactory.class.getName().replace('.', '/'),
"bootstrap",
bootstrapMethodType.toMethodDescriptorString());

cw.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Object", null);

mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, methodName, "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitInvokeDynamicInsn(
"adder",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
bootstrapMethodHandle
);
mv.visitInsn(ARETURN);
mv.visitMaxs(2,1);
mv.visitEnd();

cw.visitEnd();

return cw.toByteArray();
}


初回のinvokedynamic実行時は、bootstrap methodが呼ばれます。今回bootstrap methodとして登録したのは以下です。bootstrap methodの中では、呼び出し先メソッド(IntegerOps.adder(Integer, Integer))を探してきて、そのメソッドのハンドルを持つcall siteを返しています。


MyMethodFactory.java

  public static CallSite bootstrap(MethodHandles.Lookup callerClass,

String dynMethodName,
MethodType dynMethodType) throws Throwable
{
System.out.println("In bootstrap method: " + callerClass + ", " + dynMethodName + ", " + dynMethodType);
MethodHandle mh;
if (dynMethodType.parameterCount() == 2) {
mh = callerClass.findStatic(
IntegerOps.class,
"adder",
MethodType.methodType(Integer.class, Integer.class, Integer.class));
} else {
mh = callerClass.findStatic(
IntegerOps.class,
"adder",
MethodType.methodType(Integer.class, Integer.class, Integer.class, Integer.class));
}

if (!dynMethodType.equals(mh.type())) {
mh = mh.asType(dynMethodType);
}

return new ConstantCallSite(mh);
}


最終的に、JVMは、IntegerOps.addr(Integer, Integer)を呼び出し先メソッドとして認識し、リンクして、呼び出します。


IntegerOps.java

  public static Integer adder(Integer x, Integer y) {

return x + y;
}
public static Integer adder(Integer x, Integer y, Integer z) {
return x + y + z;
}

これにより、メソッド呼び出し先の決定をアプリケーションで制御できるようになります。しかも、呼び出し先メソッドはJavaに限らないらしい。さらに、メソッド呼び出し先の「リンク」を(PythonやRubyのように)実行時に行うため、動的な型付け言語へも対応しやすくなるという利点があります。こちらの詳細は動的型付け言語のコンパイルの課題により詳しく載っています。

以上


参考文献