Fight the Future

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

プレゼン、ボランティアコーチします!

勉強会で話したいけど、プレゼンが初めて、苦手という方に無償でコーチします!

  • スライドのレビュー
  • 録画リハへのアドバイス

Twitter@jyukutyoまでメンションでもDMでも。 デブサミやJJUG CCCなど200人規模で登壇しました。海外での登壇も短いながらあり。デブサミ2017では公募スピーカー1位でした!

JITコンパイルが使うコードキャッシュ

前回JITコンパイラで"コンパイルしたコードは戻り値で返すのではなくコードキャッシュにインストールする"と書きました。

jyukutyo.hatenablog.com

ではコードキャッシュとは何でしょう?単純化して言うとJITコンパイル後のマシンコードをキャッシュする場所です。

コードキャッシュについてはJava Magazineの2014 JULY/AUGUSTにあるBen Evansさんの記事がわかりやすいです。日本語訳もされていますので、リンクを載せます。

http://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-JA13-Architect-evans.pdf

Java Magazine

ところでJava Magazineはご存知ですか?Oracleが2ヶ月に1度、発行しているJava Magazineというデジタル発行物です。80ページほどあり、記事が非常に良質です。英語ですが、2,3ヶ月後に日本語訳も出ます。ただし、日本語は全記事の翻訳ではなく半分ほどです。読みやすい英語ですので、英語版をおすすめします。

英語版 http://www.oracle.com/technetwork/java/javamagazine/index.html

日本語版 http://www.oracle.com/technetwork/jp/articles/java/overview/index.html

さて、コードキャッシュに戻ります。先程の記事には、

マシン・コードは、中心的な要素である CodeCache(C++ オブジェクト)内に配置されます。CodeCache はCodeBlob インスタンスコンパイル後のメソッド・コードの表現)を保持するヒープのような構造体です。コード・ブロブがコード・キャッシュ内に配置されると、実行中のシステムが、インタプリタ・モードから、新しくコンパイルされたコードを使用するモードに切り替えられます

と説明があります。

コードキャッシュのデフォルトでの最大サイズはJava 8では240MB*1です。-XX:ReservedCodeCacheSizeで変更できます。あえてコードキャッシュを小さくして、キャッシュをフルにしてみましょう。

-XX:ReservedCodeCacheSize=1Mにしてみます。

Invalid ReservedCodeCacheSize: 1024K. Must be at least InitialCodeCacheSize=2496K.

2496K以上でないと上記のエラーとなります。-XX:ReservedCodeCacheSize=2496Kにします。フルにするためにはそれなりにJITコンパイルが走るアプリケーションである必要があります。start.spring.ioからSpring BootのWebアプリケーションを作成し、そのまま起動してみました。

$ java -XX:ReservedCodeCacheSize=2496K -jar demo-0.0.1-SNAPSHOT.jar
Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=
CodeCache: size=2496Kb used=1980Kb max_used=1983Kb free=515Kb
 bounds [0x0000000103db8000, 0x0000000104028000, 0x0000000104028000]
 total_blobs=1104 nmethods=623 adapters=288
 compilation: disabled (not enough contiguous free space left)

"CodeCache is full. Compiler has been disabled."となるので、キャッシュがフルになりJITコンパイルができない状況となっています。

さらに-XX:+PrintCompilationもつけてみます。

$ java -XX:+PrintCompilation -XX:ReservedCodeCacheSize=2496K -jar demo-0.0.1-SNAPSHOT.jar
...
 38826 2193   !   4       java.net.URL::<init> (543 bytes)   COMPILE SKIPPED: code cache is full
...

ものすごい量が出力されますが、"COMPILE SKIPPED: code cache is full"という出力がいくつも出ています。本番アプリケーションであれば、性能劣化となります。

せっかくなのでGraalのコードも見ます。org.graalvm.compiler.hotspot.CompilationTask#installMethod()でマシンコードをコードキャッシュにインストールしています。

    private void installMethod(DebugContext debug, final CompilationResult compResult) {
        final CodeCacheProvider codeCache = jvmciRuntime.getHostJVMCIBackend().getCodeCache();
        HotSpotBackend backend = compiler.getGraalRuntime().getHostBackend();
        installedCode = null;
        Object[] context = {new DebugDumpScope(getIdString(), true), codeCache, getMethod(), compResult};
        try (DebugContext.Scope s = debug.scope("CodeInstall", context)) {
            installedCode = (HotSpotInstalledCode) backend.createInstalledCode(debug, getRequest().getMethod(), getRequest(), compResult,
                            getRequest().getMethod().getSpeculationLog(), null, installAsDefault, context);
        } catch (Throwable e) {
            throw debug.handle(e);
        }
    }

org.graalvm.compiler.core.target.Backend#createInstalledCode()です。

    public InstalledCode createInstalledCode(DebugContext debug, ResolvedJavaMethod method, CompilationRequest compilationRequest, CompilationResult compilationResult,
                    SpeculationLog speculationLog, InstalledCode predefinedInstalledCode, boolean isDefault, Object[] context) {
...
            InstalledCode installedCode = getProviders().getCodeCache().installCode(method, compiledCode, predefinedInstalledCode, speculationLog, isDefault);
...

jdk.vm.ci.hotspot.HotSpotCodeCacheProvider#installCode()です。

    public InstalledCode installCode(ResolvedJavaMethod method, CompiledCode compiledCode, InstalledCode installedCode, SpeculationLog log, boolean isDefault) {
...
        int result = runtime.getCompilerToVM().installCode(target, (HotSpotCompiledCode) compiledCode, resultInstalledCode, speculationLog);
...

jdk.vm.ci.hotspot.CompilerToVM#installCode()です。

/**
     * Installs the result of a compilation into the code cache.
     *
     * @param target the target where this code should be installed
     * @param compiledCode the result of a compilation
     * @param code the details of the installed CodeBlob are written to this object
     * @return the outcome of the installation which will be one of
     *         {@link HotSpotVMConfig#codeInstallResultOk},
     *         {@link HotSpotVMConfig#codeInstallResultCacheFull},
     *         {@link HotSpotVMConfig#codeInstallResultCodeTooLarge},
     *         {@link HotSpotVMConfig#codeInstallResultDependenciesFailed} or
     *         {@link HotSpotVMConfig#codeInstallResultDependenciesInvalid}.
     * @throws JVMCIError if there is something wrong with the compiled code or the associated
     *             metadata.
     */
    native int installCode(TargetDescription target, HotSpotCompiledCode compiledCode, InstalledCode code, HotSpotSpeculationLog speculationLog);

nativeメソッドでした。jvmciCompilerToVM.cppです。

JNINativeMethod CompilerToVM::methods[] = {
...
  {CC "installCode",                                  CC "(" TARGET_DESCRIPTION HS_COMPILED_CODE INSTALLED_CODE HS_SPECULATION_LOG ")I",    FN_PTR(installCode)},
...

JNIですね。ここから先は僕にはわかりません。