1. 概要

JVM を解釈し、実行時にバイトコードを実行します。 さらに、Just-in-Time(JIT)コンパイルを利用してパフォーマンスを向上させます。

以前のバージョンのJavaでは、HotspotJVMで使用可能な2種類のJITコンパイラーから手動で選択する必要がありました。 1つはアプリケーションの起動を高速化するように最適化されており、もう1つは全体的なパフォーマンスを向上させます。 Java 7は、両方の長所を実現するために、階層型コンパイルを導入しました。

このチュートリアルでは、クライアントとサーバーのJITコンパイラについて説明します。 階層化されたコンパイルとその5つのコンパイルレベルを確認します。 最後に、コンパイルログを追跡することにより、メソッドのコンパイルがどのように機能するかを確認します。

2. JITコンパイラ

JITコンパイラは、頻繁に実行されるセクションのネイティブコードにバイトコードをコンパイルします。 これらのセクションはホットスポットと呼ばれるため、HotspotJVMという名前が付けられています。 その結果、Javaは完全にコンパイルされた言語と同様のパフォーマンスで実行できます。 JVMで使用可能な2種類のJITコンパイラーを見てみましょう。

2.1. C1 –クライアントコンパイラ

クライアントコンパイラはC1とも呼ばれ、起動時間の短縮用に最適化されたJITコンパイラの一種です。 できるだけ早くコードを最適化してコンパイルしようとします。

これまで、C1は、短命のアプリケーションや、起動時間が機能以外の重要な要件であるアプリケーションに使用されていました。 Java 8より前は、C1コンパイラを使用するために-clientフラグを指定する必要がありました。 ただし、Java 8以降を使用している場合、このフラグは効果がありません。

2.2. C2 –サーバーコンパイラ

サーバーコンパイラはC2とも呼ばれ、全体的なパフォーマンスを向上させるために最適化されたJITコンパイラの一種です。 C2は、C1と比較して、より長い期間にわたってコードを監視および分析します。 これにより、C2はコンパイルされたコードでより良い最適化を行うことができます。

これまで、長時間実行されるサーバー側アプリケーションにはC2を使用していました。 Java 8より前は、C2コンパイラを使用するために-serverフラグを指定する必要がありました。 ただし、このフラグはJava8以降では効果がありません。

Graal JITコンパイラは、C2の代わりにJava10以降でも使用できることに注意してください。 C2とは異なり、Graalはジャストインタイムと ahead-of-time コンパイルモードの両方で実行して、ネイティブコードを生成できます。

3. 階層型コンパイル

C2コンパイラは、同じメソッドをコンパイルするために、多くの場合、より多くの時間とより多くのメモリを消費します。 ただし、C1によって生成されるコードよりも最適化されたネイティブコードが生成されます。

階層型コンパイルの概念は、Java7で最初に導入されました。 その目標は、 C1コンパイラとC2コンパイラを組み合わせて使用し、高速起動と良好な長期パフォーマンスの両方を実現することでした

3.1. 両方の長所

アプリケーションの起動時に、JVMは最初にすべてのバイトコードを解釈し、それに関するプロファイリング情報を収集します。 次に、JITコンパイラは、収集されたプロファイリング情報を利用してホットスポットを見つけます。

まず、JITコンパイラは、頻繁に実行されるコードのセクションをC1でコンパイルして、ネイティブコードのパフォーマンスにすばやく到達します。 その後、より多くのプロファイリング情報が利用可能になると、C2が起動します。 C2は、パフォーマンスを向上させるために、より積極的で時間のかかる最適化を使用してコードを再コンパイルします。

要約すると、 C1はパフォーマンスをより速く改善し、C2はホットスポットに関するより多くの情報に基づいてパフォーマンスをより良く改善します

3.2. 正確なプロファイリング

階層型コンパイルの追加の利点は、より正確なプロファイリング情報です。 階層型コンパイルの前に、JVMは解釈中にのみプロファイリング情報を収集していました。

階層型コンパイルを有効にすると、 JVMは、C1コンパイル済みコードに関するプロファイリング情報も収集します。 コンパイルされたコードはパフォーマンスが向上するため、JVMがより多くのプロファイリングサンプルを収集できるようになります。

3.3. コードキャッシュ

コードキャッシュは、JVMがネイティブコードにコンパイルされたすべてのバイトコードを格納するメモリ領域です。 階層化されたコンパイルにより、キャッシュする必要のあるコードの量が最大4倍になりました。

Java 9以降、JVMはコードキャッシュを次の3つの領域に分割します。

  • 非メソッドセグメント– JVM内部関連コード(約5 MB、-XX:NonNMethodCodeHeapSize を介して構成可能)
  • プロファイルコードセグメント–寿命が短い可能性のあるC1コンパイル済みコード(デフォルトで約122 MB、 -XX:ProfiledCodeHeapSize を介して構成可能)
  • プロファイルされていないセグメント–潜在的に長いライフタイムを持つC2コンパイル済みコード(デフォルトで同様に122 MB、 -XX:NonProfiledCodeHeapSize を介して構成可能)

セグメント化されたコードキャッシュは、コードの局所性を改善し、メモリの断片化を減らすのに役立ちます。 したがって、全体的なパフォーマンスが向上します。

3.4. 最適化解除

C2でコンパイルされたコードは高度に最適化されており、長寿命ですが、最適化を解除することができます。 その結果、JVMは一時的に解釈にロールバックします。

最適化解除は、コンパイラの楽観的な仮定が間違っていることが証明された場合に発生します。たとえば、プロファイル情報がメソッドの動作と一致しない場合:

この例では、ホットパスが変更されると、JVMはコンパイルおよびインライン化されたコードを最適化解除します。

4. コンパイルレベル

JVMは1つのインタープリターと2つのJITコンパイラーでのみ動作しますが、5つの可能なレベルのコンパイルがあります。 この背後にある理由は、C1コンパイラが3つの異なるレベルで動作できるためです。 これらの3つのレベルの違いは、実行されるプロファイリングの量にあります。

4.1. レベル0–解釈されたコード

最初に、JVMはすべてのJavaコードを解釈します。 この初期段階では、通常、コンパイルされた言語と比較してパフォーマンスはそれほど良くありません。

ただし、JITコンパイラはウォームアップフェーズの後に起動し、実行時にホットコードをコンパイルします。 JITコンパイラは、このレベルで収集されたプロファイリング情報を利用して最適化を実行します。

4.2. レベル1–単純なC1コンパイル済みコード

このレベルでは、JVMはC1コンパイラーを使用してコードをコンパイルしますが、プロファイリング情報は収集しません。 JVMは、些細なと見なされるメソッドにレベル1を使用します。

メソッドの複雑さが低いため、C2コンパイルでは高速化できません。 したがって、JVMは、さらに最適化できないコードのプロファイリング情報を収集する意味はないと結論付けます。

4.3. レベル2–限定されたC1コンパイル済みコード

レベル2では、JVMはライトプロファイリングを備えたC1コンパイラを使用してコードをコンパイルします。 JVMは、C2キューがいっぱいになるとこのレベルを使用します。 目標は、パフォーマンスを向上させるために、できるだけ早くコードをコンパイルすることです。

その後、JVMは完全なプロファイリングを使用してレベル3でコードを再コンパイルします。 最後に、C2キューのビジー状態が緩和されると、JVMはそれをレベル4で再コンパイルします。

4.4. レベル3–完全なC1コンパイル済みコード

レベル3では、JVMは完全なプロファイリングを備えたC1コンパイラーを使用してコードをコンパイルします。 レベル3は、デフォルトのコンパイルパスの一部です。 したがって、JVMは、些細なメソッドを除くすべての場合、またはコンパイラキューがいっぱいの場合でそれを使用します

JITコンパイルで最も一般的なシナリオは、解釈されたコードがレベル0からレベル3に直接ジャンプすることです。

4.5. レベル4–C2コンパイル済みコード

このレベルでは、JVMはC2コンパイラを使用してコードをコンパイルし、長期的なパフォーマンスを最大化します。 レベル4もデフォルトのコンパイルパスの一部です。 JVMは、このレベルを使用して、些細なメソッドを除くすべてのメソッドをコンパイルします。

レベル4コードが完全に最適化されていると見なされると、JVMはプロファイリング情報の収集を停止します。 ただし、コードを最適化解除してレベル0に戻すことを決定する場合があります。

5. コンパイルパラメータ

階層型コンパイルは、Java 8 以降、デフォルトでが有効になっています。 無効にする強い理由がない限り、使用することを強くお勧めします。

5.1. 階層型コンパイルの無効化

–XX:-TieredCompilation flag を設定すると、階層型コンパイルを無効にできます。このフラグを設定すると、JVMはコンパイルレベル間を移行しません。 そのため、使用するJITコンパイラ(C1またはC2)を選択する必要があります。

明示的に指定されていない限り、JVMはCPUに基づいて使用するJITコンパイラを決定します。 マルチコアプロセッサまたは64ビットVMの場合、JVMはC2を選択します。 C2を無効にし、プロファイリングオーバーヘッドなしでC1のみを使用するために、 -XX:TieredStopAtLevel =1パラメーターを適用できます。

両方のJITコンパイラーを完全に無効にし、インタープリターを使用してすべてを実行するために、-Xintフラグを適用できます。 ただし、 JITコンパイラを無効にすると、パフォーマンスに悪影響が及ぶことに注意してください。

5.2. レベルのしきい値の設定

コンパイルしきい値は、コードがコンパイルされる前のメソッド呼び出しの数です。 階層型コンパイルの場合、コンパイルレベル2〜4にこれらのしきい値を設定できます。 たとえば、パラメータ -XX:Tier4CompileThreshold =10000を設定できます。

特定のJavaバージョンで使用されるデフォルトのしきい値を確認するために、 -XX:+PrintFlagsFinalフラグを使用してJavaを実行できます。

java -XX:+PrintFlagsFinal -version | grep CompileThreshold
intx CompileThreshold = 10000
intx Tier2CompileThreshold = 0
intx Tier3CompileThreshold = 2000
intx Tier4CompileThreshold = 15000

階層型コンパイルが有効になっている場合、JVMは汎用のCompileThresholdパラメーターを使用しないことに注意してください

6. メソッドのコンパイル

次に、メソッドのコンパイルのライフサイクルを見てみましょう。

要約すると、JVMは、呼び出しが Tier3CompileThreshold に達するまで、最初にメソッドを解釈します。 次に、プロファイリング情報の収集を継続しながら、C1コンパイラを使用してメソッドをコンパイルします。 最後に、JVMは、呼び出しが Tier4CompileThreshold に達すると、C2コンパイラを使用してメソッドをコンパイルします。 最終的に、JVMはC2コンパイル済みコードを最適化解除することを決定する場合があります。 これは、完全なプロセスが繰り返されることを意味します。

6.1. コンパイルログ

デフォルトでは、JITコンパイルログは無効になっています。 それらを有効にするには、 -XX:+PrintCompilationフラグを設定します。 コンパイルログの形式は次のとおりです。

  • タイムスタンプ–アプリケーションの起動からのミリ秒単位
  • コンパイルID–コンパイルされた各メソッドのインクリメンタルID
  • 属性–5つの可能な値を持つコンパイルの状態:
    • %–スタック上の置換が発生しました
    • s –メソッドは同期されます
    • ! –メソッドに例外ハンドラーが含まれている
    • b –コンパイルがブロッキングモードで発生しました
    • n –コンパイルによりラッパーがネイティブメソッドに変換されました
  • コンパイルレベル– 0〜4
  • メソッド名
  • バイトコードサイズ
  • 最適化解除インジケーター– 2つの可能な値:
    • 参加しない–標準のC1最適化解除またはコンパイラの楽観的な仮定が間違っていることが証明された
    • Made zombie –ガベージコレクターがコードキャッシュからスペースを解放するためのクリーンアップメカニズム

6.2. 例

簡単な例で、メソッドのコンパイルのライフサイクルを示しましょう。 まず、JSONフォーマッターを実装するクラスを作成します。

public class JsonFormatter implements Formatter {

    private static final JsonMapper mapper = new JsonMapper();

    @Override
    public <T> String format(T object) throws JsonProcessingException {
        return mapper.writeValueAsString(object);
    }

}

次に、同じインターフェイスを実装するが、XMLフォーマッターを実装するクラスを作成します。

public class XmlFormatter implements Formatter {

    private static final XmlMapper mapper = new XmlMapper();

    @Override
    public <T> String format(T object) throws JsonProcessingException {
        return mapper.writeValueAsString(object);
    }

}

次に、2つの異なるフォーマッター実装を使用するメソッドを記述します。 ループの前半では、JSON実装を使用してから、残りの部分をXML実装に切り替えます。

public class TieredCompilation {

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1_000_000; i++) {
            Formatter formatter;
            if (i < 500_000) {
                formatter = new JsonFormatter();
            } else {
                formatter = new XmlFormatter();
            }
            formatter.format(new Article("Tiered Compilation in JVM", "Baeldung"));
        }
    }

}

最後に、 -XX:+ PrintCompilation フラグを設定し、mainメソッドを実行して、コンパイルログを確認します。

6.3. レビューログ

3つのカスタムクラスとそのメソッドのログ出力に焦点を当てましょう。

最初の2つのログエントリは、JVMがmainメソッドとformatメソッドのJSON実装をレベル3でコンパイルしたことを示しています。 したがって、両方のメソッドはC1コンパイラによってコンパイルされました。 C1でコンパイルされたコードは、最初に解釈されたバージョンを置き換えました。

567  714       3       com.baeldung.tieredcompilation.JsonFormatter::format (8 bytes)
687  832 %     3       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes)
A few hundred milliseconds later, the JVM compiled both methods on level 4. Hence, the C2 compiled versions replaced the previous versions compiled with C1:
659  800       4       com.baeldung.tieredcompilation.JsonFormatter::format (8 bytes)
807  834 %     4       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes)

ほんの数ミリ秒後、最適化解除の最初の例が表示されます。 ここで、JVMはC1コンパイル済みバージョンを廃止(参加していない)とマークしました。

812  714       3       com.baeldung.tieredcompilation.JsonFormatter::format (8 bytes)   made not entrant
838 832 % 3 com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes) made not entrant

しばらくすると、最適化解除の別の例に気付くでしょう。 このログエントリは、JVMが完全に最適化されたC2コンパイル済みバージョンを廃止(参加していない)とマークしたため、興味深いものです。 つまり、 JVMは、それが無効であることが検出されたときに、完全に最適化されたコードをロールバックしました

1015  834 %     4       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes)   made not entrant
1018  800       4       com.baeldung.tieredcompilation.JsonFormatter::format (8 bytes)   made not entrant

次に、formatメソッドのXML実装を初めて確認します。 JVMは、mainメソッドとともにレベル3でコンパイルしました。

1160 1073       3       com.baeldung.tieredcompilation.XmlFormatter::format (8 bytes)
1202 1141 %     3       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes)

数百ミリ秒後、JVMは両方のメソッドをレベル4でコンパイルしました。 ただし、今回は、mainメソッドで使用されたのはXML実装です。

1341 1171       4       com.baeldung.tieredcompilation.XmlFormatter::format (8 bytes)
1505 1213 %     4       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes

以前と同じように、数ミリ秒後、JVMはC1コンパイル済みバージョンを廃止(参加していない)とマークしました。

1492 1073       3       com.baeldung.tieredcompilation.XmlFormatter::format (8 bytes)   made not entrant
1508 1141 %     3       com.baeldung.tieredcompilation.TieredCompilation::main @ 2 (58 bytes)   made not entrant

JVMは、プログラムが終了するまで、レベル4のコンパイル済みメソッドを使用し続けました。

7. 結論

この記事では、JVMでの階層型コンパイルの概念について説明しました。 2種類のJITコンパイラーと、階層型コンパイルで両方を使用して最良の結果を得る方法を確認しました。 5つのレベルのコンパイルを確認し、JVMパラメーターを使用してそれらを制御する方法を学びました。

例では、コンパイルログを観察することにより、メソッドのコンパイルのライフサイクル全体を調査しました。

いつものように、ソースコードはGitHubから入手できます。