【Java】Java 17環境でNashornの高速化に挑戦!

未分類

最初に、この挑戦の結果は失敗していますが。異なる環境では成功する可能性もあるので備忘録として残しておきます。

導入:なぜ今、Nashornの高速化が必要なのか

現在、Java 17以降でJavaScriptを動かす標準はGraalJSです。しかし、業務で利用している特定の外部ライブラリが内部でNashornに強く依存しており、自力でGraalJSへ移行できないという状況に直面しました。

このライブラリ経由でのスクリプト実行が無視できないボトルネックとなっており、システム全体のパフォーマンスを損なっていました。非推奨技術であるNashornをJava 17環境でいかに「延命」させ、高速化させるかに知恵を絞り、全力で挑戦した記録を残します。


1. 実行基盤の準備:スタンドアロン版Nashorn

JDK 17にはNashornが同梱されていないため、まずは最新のスタンドアロン版を依存関係に組み込みます。

XML

<dependency>
    <groupId>org.openjdk.nashorn</groupId>
    <artifactId>nashorn-core</artifactId>
    <version>15.4</version> </dependency>

2. コードレベルの挑戦:CompiledScript による解析の排除

Nashornの実行が遅い最大の要因は、呼び出しのたびに行われるスクリプトの解析(パース)です。同じロジックを繰り返し実行する業務ロジックでは、Compilable インターフェースを利用したキャッシュ化を試みました。


Java

import javax.script.*;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;

public class NashornSpeedUp {
    private static final CompiledScript CACHED_SCRIPT;

    static {
        try {
            NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
            // 最適化オプションをエンジン生成時に渡す
            ScriptEngine engine = factory.getScriptEngine(new String[]{"--optimistic-types=true"});
            
            if (engine instanceof Compilable compilable) {
                // スクリプトを事前にバイトコードへコンパイル
                CACHED_SCRIPT = compilable.compile("function calculate(v) { return v * 1.08; }; calculate(input);");
            } else {
                throw new RuntimeException("Compilation not supported");
            }
        } catch (ScriptException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public void run(double val) throws ScriptException {
        Bindings bindings = new SimpleBindings();
        bindings.put("input", val);
        // コンパイル済みスクリプトを実行
        CACHED_SCRIPT.eval(bindings);
    }
}

3. エンジンオプションの攻め:内部最適化フラグ

NashornScriptEngineFactory を通じて、Nashorn内部の実行モードを切り替え、JITコンパイルの効率化を図りました。

  • --optimistic-types=true: 「おそらくこの変数はintだろう」といった楽観的な型推論を行い、JITコンパイルを最適化するフラグ。
  • --persistent-code-cache=true: コンパイル済みのバイトコードをディスクに永続化し、起動直後からの高速動作を狙う設定。

4. インフラレベルの挑戦:JVM引数(VMフラグ)の調整

Nashornは実行時にJavaのバイトコードを大量に生成するため、JVM側の「器」を拡張してメモリ起因の遅延を排除しようと試みました。

# JITコンパイルされたコードの格納先を拡張
-XX:ReservedCodeCacheSize=512m

# メタスペースの上限を適切に設定し、GCの頻度を抑える
-XX:MaxMetaspaceSize=512m

# システムプロパティ経由でキャッシュ設定をグローバル適用
-Dnashorn.args="--persistent-code-cache=true --code-cache-dir=./nashorn-cache"

まとめ:挑戦の結果

結論から言えば、今回の挑戦は「動作に目立った変化なし」という失敗に終わりました。

外部からのアクションでライブラリ内の動作の改善までは手が届きませんでした。

設定を入れるだけの簡易的な取り組みでは、カプセル化されたライブラリの壁は越えられない。改めて、根深い依存関係を持つライブラリの選定には慎重であるべきだという痛い教訓を得る結果となりました。