1. 概要

JVMは、これまでに構築された中で最も古いが強力な仮想マシンの1つです。

この記事では、JVMをウォームアップすることの意味とその方法について簡単に説明します。

2. JVMアーキテクチャの基本

新しいJVMプロセスが開始されるたびに、必要なすべてのクラスがClassLoaderのインスタンスによってメモリにロードされます。 このプロセスは、次の3つのステップで行われます。

  1. ブートストラップクラスのロード:ブートストラップクラスローダー」は、Javaコードとjava.lang.Objectなどの重要なJavaクラスをメモリにロードします。 これらのロードされたクラスは、 JRE \ lib \rt.jarにあります。
  2. 拡張クラスのロード:ExtClassLoaderは、java.ext.dirsパスにあるすべてのJARファイルのロードを担当します。 開発者がJARを手動で追加する非Mavenまたは非Gradleベースのアプリケーションでは、これらのクラスはすべてこのフェーズでロードされます。
  3. アプリケーションクラスの読み込み:AppClassLoaderは、アプリケーションクラスパスにあるすべてのクラスを読み込みます。

この初期化プロセスは、遅延読み込みスキームに基づいています。

3. JVMのウォーミングアップとは

クラスの読み込みが完了すると、すべての重要なクラス(プロセスの開始時に使用される)が JVMキャッシュ(ネイティブコード)にプッシュされます。これにより、実行時にそれらにすばやくアクセスできるようになります。 他のクラスは、リクエストごとにロードされます。

Java Webアプリケーションに対して行われる最初の要求は、多くの場合、プロセスの存続期間中の平均応答時間よりも大幅に遅くなります。 このウォームアップ期間は通常、レイジークラスのロードとジャストインタイムコンパイルに起因する可能性があります。

このことを念頭に置いて、低レイテンシのアプリケーションでは、すべてのクラスを事前にキャッシュして、実行時にアクセスしたときにすぐに利用できるようにする必要があります。

JVMを調整するこのプロセスは、ウォーミングアップと呼ばれます。

4. 階層型コンパイル

JVMの健全なアーキテクチャのおかげで、頻繁に使用されるメソッドは、アプリケーションのライフサイクル中にネイティブキャッシュにロードされます。

このプロパティを利用して、アプリケーションの起動時に重要なメソッドをキャッシュに強制的にロードできます。 その範囲で、 TieredCompilation:という名前のVM引数を設定する必要があります

-XX:CompileThreshold -XX:TieredCompilation

通常、VMはインタープリターを使用して、コンパイラーに供給されるメソッドに関するプロファイリング情報を収集します。 階層型スキームでは、インタープリターに加えて、クライアントコンパイラーを使用して、自身に関するプロファイリング情報を収集するメソッドのコンパイル済みバージョンを生成します。

コンパイルされたコードはインタプリタされたコードよりも大幅に高速であるため、プログラムはプロファイリングフェーズでより優れたパフォーマンスで実行されます。

このVM引数が有効になっているJBossおよびJDKバージョン7で実行されているアプリケーションは、文書化されたバグが原因で、しばらくするとクラッシュする傾向があります。 この問題は、JDKバージョン8で修正されています。

ここで注意すべきもう1つのポイントは、ロードを強制するために、実行されるすべての(またはほとんどの)クラスにアクセスする必要があることを確認する必要があるということです。 これは、単体テスト中にコードカバレッジを決定することに似ています。 カバーされるコードが多いほど、パフォーマンスは向上します。

次のセクションでは、これを実装する方法を示します。

5. 手動実装

JVMをウォームアップするための代替手法を実装する場合があります。 この場合、単純な手動ウォームアップには、アプリケーションの起動後すぐに、さまざまなクラスの作成を数千回繰り返すことが含まれる場合があります。

まず、通常のメソッドを使用してダミークラスを作成する必要があります。

public class Dummy {
    public void m() {
    }
}

次に、アプリケーションの起動と同時に少なくとも100000回実行される静的メソッドを持つクラスを作成する必要があります。実行するたびに、前に作成した前述のダミークラスの新しいインスタンスが作成されます。

public class ManualClassLoader {
    protected static void load() {
        for (int i = 0; i < 100000; i++) {
            Dummy dummy = new Dummy();
            dummy.m();
        }
    }
}

ここで、パフォーマンスゲインを測定するには、メインクラスを作成する必要があります。 このクラスには、 ManualClassLoaderのload()メソッドへの直接呼び出しを含む静的ブロックが1つ含まれています。

main関数内で、 ManualClassLoaderのload()メソッドをもう一度呼び出し、関数呼び出しの直前と直後のシステム時間をナノ秒単位でキャプチャします。 最後に、これらの時間を差し引いて、実際の実行時間を取得します。

アプリケーションを2回実行する必要があります。 1回は静的ブロック内でload()メソッド呼び出しを使用し、もう1回はこのメソッド呼び出しを使用しません。

public class MainApplication {
    static {
        long start = System.nanoTime();
        ManualClassLoader.load();
        long end = System.nanoTime();
        System.out.println("Warm Up time : " + (end - start));
    }
    public static void main(String[] args) {
        long start = System.nanoTime();
        ManualClassLoader.load();
        long end = System.nanoTime();
        System.out.println("Total time taken : " + (end - start));
    }
}

以下の結果はナノ秒単位で再現されています。

ウォームアップあり ウォームアップなし 違い(%)
1220056 8903640 730
1083797 13609530 1256
1026025 9283837 905
1024047 7234871 706
868782 9146180 1053

予想どおり、ウォームアップアプローチでは、通常のアプローチよりもはるかに優れたパフォーマンスが示されます。

もちろん、これは非常に単純なベンチマークであり、この手法の影響に関する表面レベルの洞察を提供するだけです。 また、実際のアプリケーションでは、システム内の一般的なコードパスでウォームアップする必要があることを理解することが重要です。

6. ツール

いくつかのツールを使用してJVMをウォームアップすることもできます。 最もよく知られているツールの1つは、Java Microbenchmark Harness、JMHです。 これは通常、マイクロベンチマークに使用されます。 ロードされると、コードスニペットに繰り返しヒットし、ウォームアップの反復サイクルを監視します。

これを使用するには、pom.xmlに別の依存関係を追加する必要があります。

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.35</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.35</version>
</dependency>

JMHの最新バージョンはCentralMavenRepositoryで確認できます。

または、JMHのMavenプラグインを使用してサンプルプロジェクトを生成することもできます。

mvn archetype:generate \
    -DinteractiveMode=false \
    -DarchetypeGroupId=org.openjdk.jmh \
    -DarchetypeArtifactId=jmh-java-benchmark-archetype \
    -DgroupId=com.baeldung \
    -DartifactId=test \
    -Dversion=1.0

次に、mainメソッドを作成しましょう。

public static void main(String[] args) 
  throws RunnerException, IOException {
    Main.main(args);
}

次に、メソッドを作成し、JMHの@Benchmarkアノテーションでアノテーションを付ける必要があります。

@Benchmark
public void init() {
    //code snippet	
}

このinitメソッド内で、ウォームアップのために繰り返し実行する必要のあるコードを記述する必要があります。

7. パフォーマンスベンチマーク

過去20年間、Javaへの貢献のほとんどは、GC(ガベージコレクター)とJIT(ジャストインタイムコンパイラー)に関連していました。 オンラインで見つかったパフォーマンスベンチマークのほとんどすべては、すでにしばらくの間実行されているJVMで実行されます。 でも、

ただし、 Beihang University は、JVMのウォームアップ時間を考慮したベンチマークレポートを公開しています。 彼らはHadoopおよびSparkベースのシステムを使用して大量のデータを処理しました。

ここで、HotTubは、JVMがウォームアップされた環境を示します。

ご覧のとおり、特に比較的小さな読み取り操作の場合、スピードアップが大幅に向上する可能性があります。そのため、このデータを検討するのは興味深いことです。

8. 結論

この簡単な記事では、アプリケーションの起動時にJVMがクラスをロードする方法と、パフォーマンスを向上させるためにJVMをウォームアップする方法を示しました。

この本では、続行する場合に、このトピックに関する詳細情報とガイドラインについて説明しています。

そして、いつものように、完全なソースコードはGitHub利用できます。