Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Java 11 / JEP 309のConstant Dynamic(condy)を試す

JEP 309: Dynamic Class-File Constantsというものがあります。これはConstant Dynamic(condy)と呼ばれるものです。

http://openjdk.java.net/jeps/309

Constant Dynamic、つまり動的な定数です。Javaでは今まで(10まで)プリミティブとリテラルを持つStringだけConstant Poolに格納できました。condyでその他のものもConstant Poolで扱えるようになります。

さらに、condyでは定数値の初期化を遅延します。今までコンパイル時に値が確定しているものだけが対象でしたが、それを拡大します。

Invokedynamicとの類似性

Invokedynamic(indy)というものがJava 7からあります。JVMで動的な呼び出しを実現するものです。コードの実行時にInvokedynamicに遭遇すると、初回のみブートストラップメソッドを呼び出してCallSiteオブジェクトを生成し、以降同じ内容のinvokedynamicはそのCallSiteオブジェクトを使います。

CallSiteは初回のinvokedynamicの実行時まで生成が遅延されている、と言えます。

Invokedynamicはメソッド呼び出しを対象としていました。condyはこの仕組みを定数に対して適用したものと考えてよいです。その定数の初回使用時にブートストラップメソッドを使い定数の値を確定させます。ブートストラップメソッドからはあらゆるオブジェクトを返せるので、こうしてConstant PoolでプリミティブとString以外を扱えるようになるのです(nullもです)。

参考セッション動画

JVMLS 2018でOpenJ9の開発者Dan HeidingaさんとPaul Sandozさんによるcondyのセッションがありました。私は現地にて聴きました。録画も公開されています。

www.youtube.com

Java 11で試す

condyの実装はすでに終わっています。この記事の時点ではJava 11はEarly-Accessビルドですが、condyに対応しています。

http://jdk.java.net/11/

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11+28)
OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode)

condy自体は実装が完了しているとは言え、現時点ではコンパイラがcondyを生成するJavaのコードはありません。JVM側はcondy対応が完了しcondyを実行できるが、Java側でまだ使っていないということです。indyリリース時と同じ感じです。

そのため、ここではバイトコード操作でcondyを使うクラスを出力します。バイトコード操作ライブラリとしてByte Buddyを使います。Byte Buddyは1.8.16以降Java 11に対応しており、condyやnestmateといった機能に対応しています。このことについてはInfoQ.comの記事があります。翻訳者は私です。

www.infoq.com

    public static class TargetClass {
        public Object targetMethod() {
            return null;
        }
    }

TargetClassはバイトコード操作のために作っただけです。Byte BuddyにはTargetClassのサブクラスを作成させ、そのサブクラスにcondyを入れます。

    public static class CondyClass {
        public static CondyClass make() {
            return new CondyClass();
        }
    }

CondyClassは定数の値となるクラスです。condyでこのmake()メソッドの戻り値が定数値となります。ここでは単純にCondyClassオブジェクトにしました。

public class CondySample {

    public static void main(String[] args) throws Exception {
        DynamicType.Unloaded<TargetClass> unloaded = new ByteBuddy()
                .subclass(TargetClass.class)
                .method(isDeclaredBy(TargetClass.class))
                .intercept(FixedValue.value(JavaConstant.Dynamic.ofInvocation(CondyClass.class.getMethod("make"))))
                .make();
        unloaded
                .saveIn(Paths.get("./temp").toFile());

        TargetClass targetClass = unloaded
                .load(TargetClass.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded()
                .getDeclaredConstructor()
                .newInstance();

        System.out.println(targetClass.targetMethod());
    }
}

main()メソッドの前半でTargetClassのサブクラスを作成し、メソッド呼び出しをインターセプトして固定値としてcondyで生成する定数を返すよう変更します。Byte Buddyではcondy関連はnet.bytebuddy.utility.JavaConstant.Dynamicから利用します。tempディレクトリに作成したクラスのクラスファイルを出力します。

    /**
     * Represents a dynamically resolved constant pool entry of a class file. This feature is supported for class files in version 11 and newer.
     */
    class Dynamic implements JavaConstant {
    }

後半は、作成したTargetClassのサブクラスをクラスローダにロードさせ、ClassクラスからnewInstance()でインスタンス化し、targetMethod()メソッドを呼び出します。もともとのTargetClassではreturn null;していましたが、サブクラスではcondyで作成した定数値を返すようオーバーライドしています。

これを実行します。エラーになります。

Exception in thread "main" java.lang.UnsupportedOperationException: This feature requires ASM7
    at net.bytebuddy.jar.asm.MethodVisitor.visitLdcInsn(MethodVisitor.java:547)

Java 11対応はまだ実験的機能のようで、オプション-Dnet.bytebuddy.experimental=trueをつけて実行する必要があります。

com.sakatakoichi.CondySample$CondyClass@4cc451f2

targetMethod()を呼び出した結果CondyClassクラスのインスタンスが返ってきており、それを出力しています。

バイトコード

実行しましたが、本当にcondyは利用されているのでしょうか?バイトコードを見てみましょう。tempディレクトリに保存しました。temp/com/sakatakoichi/CondySample$TargetClass$ByteBuddy$1a3Y6WFv.classといったByte Buddyがランダムに命名したクラスが出力されているはずです。javap -vします。

Constant pool:
...
  #23 = Dynamic            #0:#22         // #0:invoke:Lcom/sakatakoichi/CondySample$CondyClass;

condyでConstant PoolにDynamicというタイプが追加されています。CondyClassであるとコメントから読み取れます。

BootstrapMethods:
  0: #20 REF_invokeStatic java/lang/invoke/ConstantBootstraps.invoke:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;Ljava/lang/invoke/MethodHandle;[Ljava/lang/Object;)Ljava/lang/Object;
    Method arguments:
      #13 REF_invokeStatic com/sakatakoichi/CondySample$CondyClass.make:()Lcom/sakatakoichi/CondySample$CondyClass;

BootstrapMethods属性でcondyのConstantBootstraps.invokeを設定しているのがわかります。その引数はCondyClass.make()のメソッドハンドルです。

  #13 = MethodHandle       6:#12          // REF_invokeStatic com/sakatakoichi/CondySample$CondyClass.make:()Lcom/sakatakoichi/CondySample$CondyClass;

condyでjava.lang.invoke.ConstantBootstrapsクラスが追加されています。さまざまなメソッドがありますが、今回呼び出しているメソッドはこちらです。

    /**
     * Returns the result of invoking a method handle with the provided
     * arguments.
...
     */
    public static Object invoke(MethodHandles.Lookup lookup, String name, Class<?> type,
                                MethodHandle handle, Object... args) throws Throwable {
...
        if (type != handle.type().returnType()) {
            // Adjust the return type of the handle to be invoked while
            // preserving variable arity if present
            handle = handle.asType(handle.type().changeReturnType(type)).
                    withVarargs(handle.isVarargsCollector());
        }

        return handle.invokeWithArguments(args);
    }

最終的には、引数で渡したメソッドハンドルを呼び出し、その結果を返しています。

まとめ

Java 11で導入されるJEP 309 Constant Dynamic(condy)を試しました。現在indyを使っているラムダ式も、今後non-capturingなものはcondyを使うように変更するといったことも検討されているようです。