JvmのJIT最適化の詳細説明

1. JITの背景

高級言語をコンピューターが認識できる機械語に変換するには、コンパイルとインタープリタという 2 つの方法があることがわかっています。Java では、コードを実行するにはバイトコードにコンパイルする必要がありますが、バイトコード自体をマシン上で直接実行することはできません。

したがって、JVM には、実行時にバイトコードを解釈し、マシンコードに変換して実行するインタープリター (インタープリター) が組み込まれています。

インタプリタの実行モードは翻訳と実行を同時に行うため、実行効率が低いです。この非効率性の問題を解決するために、HotSpot では JIT テクノロジ (ジャスト イン タイム コンパイル) が導入されました。

JIT テクノロジーを使用しても、JVM は解釈と実行にインタプリタを使用します。ただし、JVM は、特定のメソッドまたはコード ブロックが実行時に頻繁に実行されることを検出すると、それを「ホット コード」としてマークします。次に、JIT は一部のホット コードをローカル マシン関連のマシン コードに変換して最適化し、次に使用するために変換されたマシン コードをキャッシュします。

JVMのJIT最適化

2. HotSpot 仮想マシンには JIT コンパイラが組み込まれています

HotSpot 仮想マシンには、次の 2 つの JIT コンパイラが組み込まれています。クライアントコンパイラとサーバーコンパイラ、それぞれクライアント側とサーバー側用です。現在の主流の HotSpot 仮想マシンでは、インタープリタはデフォルトでコンパイラの 1 つと直接動作します。

JVM がコードを実行するとき、コードのコンパイルはすぐには開始されません。まず、コードが 1 回しか実行されない場合、コードを Java バイトコードに変換するのに比べて、コードをコンパイルするのは無駄です。コードをバイトコードに変換するプロセスは、コードをコンパイルして実行するプロセスよりもはるかに高速であるためです。次に、JVM はコードのコンパイル時に最適化を実行します。メソッドまたはループがより多く実行されると、JVM はコード構造をより深く理解し、コードのコンパイル時に対応する最適化を行います。

1. クライアントコンパイラ

クライアント コンパイラ (C1 コンパイラまたはクライアント JIT とも呼ばれる) は、主に起動速度とメモリ使用量を最適化します。プログラムの先頭でコンパイルして実行可能コードを迅速に生成しますが、パフォーマンスについてはあまり最適化されていません。

2. サーバーコンパイラ

サーバー コンパイラー (C2 コンパイラーまたはサーバー JIT とも呼ばれる) は、プログラムの実行効率を向上させるために、実行時のコードのより詳細な最適化に重点を置いています。プログラムの実行中に動的解析と最適化を通じて高性能のマシンコードを生成します。

3. ローカルコンパイラモードを確認する

マシンにインストールされている JDK の JIT がどのモードを使用しているかを確認したい場合は、「java -version」コマンドを実行できます。このコマンドは、JIT コンパイラ モードも含む JDK バージョン情報を表示します。

java -version

コンパイラモード
この図は、ローカル マシンにインストールされた JDK 1.8 を示しています。JIT コンパイラ モードは次のとおりです。サーバーコンパイラ。ただし、クライアント コンパイラであってもサーバー コンパイラであっても、インタプリタとコンパイラはブレンドモード併用すると、つまり図に示されているのはミックスモード。

3. 一般的なホットスポット検出技術

JIT コンパイルをトリガーするには、まずホット コードを識別する必要があります。現在、ホット スポット検出は主にホット コードを識別するために使用されており、一般的な方法が 2 つあります。

1. カウンタベースのホットスポット検出

1 つはカウンターベースのホット スポット検出で、メソッド呼び出しの数をカウントし、特定のしきい値に達したときにメソッドをホット コードとしてマークします。この方法はシンプルかつ直接的であり、いくつかの単純なホットスポット シーンを識別するのに適しています。

2. サンプリングベースのホットスポット検出

HotSpot 仮想マシンは、各スレッドのスタックの先頭を定期的に検出してホットスポット方式を決定する方法を使用します。メソッドがスタックの最上位に頻繁に表示される場合、そのメソッドはホット メソッドとみなされます。この方法の利点はシンプルでわかりやすいことですが、欠点はメソッドの人気を正確に判断できないことです。さらに、スレッドのブロッキングやその他の理由による干渉を受けやすく、ホットスポット検出の精度に影響を与えます。

HotSpot仮想マシンではカウンタベースのホットスポット検出方式を採用しているため、メソッドごとにメソッドコールカウンタとエッジカウンタの2つのカウンタが用意されています。

2.1 メソッド呼び出しカウンター

メソッド呼び出しカウンターは、その名前が示すように、メソッドが呼び出された回数を記録するために使用されるカウンターです。メソッド呼び出しの数をカウントし、特定のしきい値に達するとメソッドをホット コードとしてマークします。

2.2 返却カウンター

エッジ カウンタは、メソッド内のループ構造 (for ループや while ループなど) が実行された回数を記録するために使用されるカウンタです。ループ構造の反復回数をカウントし、反復回数に基づいてループがホット コードであるかどうかを判断します。

これら 2 つのカウンターの目的は、HotSpot 仮想マシンがホット コードを識別し、最適化のための JIT コンパイルをトリガーできるようにすることです。メソッド呼び出しカウンタは頻繁に呼び出されるメソッドを識別するために使用され、エッジ カウンタは多数回実行されるループ構造を識別するために使用されます。これらのホットコードを特定することで、プログラムの実行効率を向上させることができます。

一般に、HotSpot 仮想マシンは、メソッド コール カウンタとエッジバック カウンタをカウンタ ベースのホットスポット検出方法として使用して、ホットスポット コードを識別し、それを最適化して Java アプリケーションのパフォーマンスを向上させます。

4. 一般的な JIT 最適化手法

1. 共通部分式の削除

共通部分式の削除は、JVM JIT コンパイラの最適化テクノロジであり、繰り返しの計算を減らし、プログラムの実行効率を向上させるために使用されます。

共通部分式とは、プログラム内で複数回出現する計算式のことで、共通部分式による最適化を省略することで、繰り返し行われる計算を 1 つの計算にまとめて、不要な計算オーバーヘッドを削減できます。

以下は、共通部分式の削除最適化を示す簡単なコード例です。

public class CommonSubexpressionEliminationDemo {
    
    
    public static void main(String[] args) {
    
    
        int a = 5;
        int b = 3;
        int c = a * b + 2; // 公共子表达式 a * b
        int d = a * b + 2; // 公共子表达式 a * b
        
        System.out.println(c);
        System.out.println(d);
    }
}

上記のコードでは、変数 c と d は両方とも同じ計算式a * b + 2 を使用すると、共通の部分式による最適化が不要になり、JVM JIT コンパイラーは繰り返される計算を 1 つの計算にマージします。

要約:

共通部分式を削除すると、計算の繰り返しが減り、プログラムの実行効率が向上します。
JVM JIT コンパイラーは、繰り返される計算式を識別し、それらを 1 つの計算に最適化します。
共通の部分式を使用して最適化を排除すると、特にループ内で同じ式を使用する場合に、不必要な計算のオーバーヘッドを削減できます。

2. メソッドのインライン化

メソッドのインライン化は、JVM JIT コンパイラーの最適化テクノロジーであり、メソッド呼び出しのコストを削減し、プログラムの実行効率を向上させるために使用されます。

メソッドのインライン化とは、メソッド呼び出しを通じてメソッドを実行するのではなく、メソッドのコードをメソッドが呼び出される場所に直接挿入することを指します。これにより、スタック フレームの作成と破棄、パラメータの受け渡し、その他の操作を含むメソッド呼び出しのオーバーヘッドを削減できます。

以下は、メソッドのインライン化の最適化を示す簡単なコード例です。

public class MethodInliningDemo {
    
    
    public static void main(String[] args) {
    
    
        int a = 5;
        int b = 3;
        int c = add(a, b); // 方法调用
        int d = a + b; // 方法内联
        
        System.out.println(c);
        System.out.println(d);
    }
    
    public static int add(int a, int b) {
    
    
        return a + b;
    }
}

上記のコードでは、変数 c はメソッド呼び出しを通じて結果を計算しますが、変数 d はメソッド コードを呼び出しサイトに直接インライン化して計算します。

要約:

メソッドのインライン化により、メソッド呼び出しのコストが削減され、プログラムの実行効率が向上します。
JVM JIT コンパイラーは、インライン化に適したメソッドを特定し、実行のために呼び出しサイトに直接挿入されるように最適化します。
メソッドのインライン最適化を使用すると、特に頻繁に呼び出されるメソッドの呼び出しコストを削減できます。
過剰なメソッドのインライン化は、コードの肥大化を引き起こし、コンパイル時間とメモリ消費量を増加させる可能性があることに注意してください。したがって、メソッドのインライン化を使用する場合、コード サイズとパフォーマンスの向上の間にはトレードオフがあります。

3. 逃走分析

エスケープ分析は、JVM JIT コンパイラの最適化テクノロジであり、オブジェクトのスコープを分析し、オブジェクトがメソッドのスコープをエスケープするかどうかを判断して、オブジェクトのメモリ割り当てを最適化するために使用されます。

エスケープ分析の目的は、メソッドからエスケープしないオブジェクトを見つけて、それらをヒープではなくスタックに割り当てて、ガベージ コレクションのオーバーヘッドを削減することです。

以下は、エスケープ分析の最適化を示す簡単なコード例です。

public class EscapeAnalysisDemo {
    
    
    public static void main(String[] args) {
    
    
        User user = createUser("Alice"); // 对象逃逸
        
        System.out.println(user.getName());
    }
    
    public static User createUser(String name) {
    
    
        return new User(name); // 对象逃逸
    }
    
    static class User {
    
    
        private String name;
        
        public User(String name) {
    
    
            this.name = name;
        }
        
        public String getName() {
    
    
            return name;
        }
    }
}

上記のコードでは、createUser メソッドで作成された User オブジェクトはメソッドのスコープをエスケープし、外部参照によって使用されます。

要約:

  • エスケープ分析は、オブジェクトのスコープを分析し、オブジェクトがメソッドのスコープをエスケープするかどうかを判断するために使用される、JVM JIT コンパイラーの最適化テクノロジーです。

  • エスケープ分析の目的は、メソッドからエスケープしないオブジェクトを見つけて、それらをヒープではなくスタックに割り当てて、ガベージ コレクションのオーバーヘッドを削減することです。

  • エスケープ解析により、ヒープ割り当てとガベージ コレクションのオーバーヘッドが削減され、プログラムの実行効率が向上します。

  • エスケープ分析は絶対に効果的な最適化テクノロジではないことに注意してください。エスケープ分析は特定のシナリオでのみ効果的であり、ほとんどのアプリケーションではヒープ割り当てとガベージ コレクションのオーバーヘッドがパフォーマンスのボトルネックではないため、エスケープ分析の用途は限定的です。

  • エスケープ分析は、JVM のパラメーターを通じて制御されます。JDK7 以降のバージョンでは、エスケープ分析はデフォルトでオンになっています。

以下は、エスケープ分析に関連するいくつかの JVM パラメーターです。

  • -XX:+DoEscapeAnalysis: エスケープ分析を有効にします。デフォルトでは有効になっています。
  • -XX:-DoEscapeAnalysis: エスケープ分析を無効にします。
  • -XX:+PrintEscapeAnalysis: エスケープ解析関連の情報を出力します。
    -XX:+EliminateLocks: エスケープ解析により不要なロックを削除します。
  • エスケープ分析の効果は特定の JVM 実装に関連していることに注意してください。JVM が異なれば、エスケープ分析のサポートおよび最適化レベルも異なる場合があります。したがって、一部の特定のシナリオでは、実際の状況に基づいて適切な調整と最適化を行う必要がある場合があります。

3.1 エスケープ解析のためのスカラー置換

スカラー置換はエスケープ分析のための最適化手法であり、オブジェクトを独立したスカラー (単一の基本型またはオブジェクト参照) に分解し、これらのスカラーをそれぞれスタックまたはレジスターに割り当て、オブジェクトの作成とアクセス操作を回避します。

スカラー置換の効果を示す簡単なコード例を次に示します。

public class ScalarReplacementDemo {
    
    
    public static void main(String[] args) {
    
    
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
    
    
            Point point = new Point(i, i); // 创建一个Point对象
            int sum = point.x + point.y; // 使用Point对象的属性进行计算
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    static class Point {
    
    
        int x;
        int y;

        public Point(int x, int y) {
    
    
            this.x = x;
            this.y = y;
        }
    }
}

上記のコードでは、ループ内に 10,000,000 個の Point オブジェクトを作成し、各オブジェクトのプロパティを追加します。エスケープ分析がオンで、スカラー置換が有効な場合、JVM は Point オブジェクトのプロパティ x と y を 2 つの独立したローカル変数に置き換えてスタック上に割り当てます。これにより、Point オブジェクトの作成とアクセスが回避されます。

要約:

  • スカラー置換は、オブジェクトを独立したスカラーに分割し、それらをスタックまたはレジスターに割り当てるエスケープ解析の最適化手法です。
  • スカラー置換により、オブジェクトの作成とアクセス操作が回避されるため、プログラムのパフォーマンスが向上します。
  • スカラー置換を有効にするには、エスケープ分析がオンになっていること、および JVM が実行時にスカラー置換を自動的に最適化することを確認する必要があります。
  • コードを記述するとき、不変オブジェクトやローカル変数などを使用するなど、適切なコード設計を通じて JVM がスカラー置換の最適化を実行できるようにすることができます。

3.2 スタック上の割り当てのエスケープ解析

スタック割り当ては、エスケープ分析のためのもう 1 つの最適化手法であり、ヒープではなくスタック上の特定のオブジェクトにメモリを割り当てます。スタック上に割り当てを行うと、ヒープ上でのオブジェクトの割り当てとリサイクルのオーバーヘッドが削減され、プログラムのパフォーマンスが向上します。

以下は、スタックへの割り当ての影響を示す簡単なコード例です。

public class StackAllocationDemo {
    
    
    public static void main(String[] args) {
    
    
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
    
    
            Point point = createPoint(i, i); // 创建一个Point对象,并返回其引用
            int sum = point.x + point.y; // 使用Point对象的属性进行计算
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    static Point createPoint(int x, int y) {
    
    
        return new Point(x, y);
    }

    static class Point {
    
    
        int x;
        int y;

        public Point(int x, int y) {
    
    
            this.x = x;
            this.y = y;
        }
    }
}

上記のコードでは、ループ内に 10,000,000 個の Point オブジェクトを作成し、各オブジェクトのプロパティを追加します。エスケープ分析がオンになっていて、スタック割り当てが有効になっている場合、JVM は Point オブジェクトのメモリをヒープではなくスタックに割り当てます。これにより、ヒープ上でのオブジェクト割り当てとリサイクルのオーバーヘッドが削減されます。

要約:

  • スタックへの割り当ては、特定のオブジェクトのメモリをヒープではなくスタックに割り当てるエスケープ分析の最適化手法です。
  • スタック上に割り当てを行うと、ヒープ上でのオブジェクトの割り当てとリサイクルのオーバーヘッドが削減され、プログラムのパフォーマンスが向上します。
  • オンスタック割り当てを有効にするには、エスケープ分析がオンになっていること、および JVM が実行時にオンスタック割り当てを自動的に最適化することを確認する必要があります。
  • コードを記述するとき、オブジェクトのスコープをメソッドに制限したり、ローカル変数を使用したりするなど、適切なコード設計を通じて JVM によるスタック割り当ての最適化を支援できます。

3.3 エスケープ解析の同期除去

同期除去はエスケープ分析のもう 1 つの最適化手法であり、コード内の同期操作を分析して、プログラムのパフォーマンスを向上させるためにこれらの同期操作を除去できるかどうかを判断します。

以下は、同期消去の効果を示す簡単なコード例です。

public class SynchronizationEliminationDemo {
    
    
    public static void main(String[] args) {
    
    
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
    
    
            synchronizedMethod();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    static void synchronizedMethod() {
    
    
        synchronized (SynchronizationEliminationDemo.class) {
    
    
            // 同步块中的代码
        }
    }
}

上記のコードでは、同期ブロックを含む synchronizedMethod メソッドを 10000000 回ループして呼び出します。エスケープ分析がオンで、同期の削除が有効な場合、JVM は synchronizedMethod メソッドが他のスレッドにエスケープしていないと判断するため、同期操作を削除でき、それによってプログラムのパフォーマンスが向上します。

要約:

  • 同期削除はエスケープ解析の最適化手法であり、コード内の同期操作を分析することにより、これらの同期操作を削除してプログラムのパフォーマンスを向上できるかどうかを判断します。
  • 同期を排除するための前提条件は、エスケープ分析がオンになっていて、同期操作が他のスレッドにエスケープされないと JVM が判断できることです。
  • 同期を排除すると、スレッド間の同期オーバーヘッドが削減され、プログラムの同時実行パフォーマンスが向上します。
    コードを記述するときに、不必要な同期操作を回避し、オブジェクトのスコープを合理的に設計することで、JVM による同期排除の最適化を支援できます。

5. JIT 最適化によって発生する可能性のある問題

JIT コンパイルの原理を理解すると、JIT の最適化は実行時に行われ、Java プロセスの開始直後には最適化がすぐに実行できるわけではないことがわかります。どのコードがホット コードであるかを判断するには、ある程度の実行時間が必要です。

したがって、JIT 最適化を開始する前に、すべてのリクエストを解釈して実行する必要がありますが、これは比較的遅いプロセスです。この問題は、アプリケーションのリクエスト量が多い場合にさらに顕著になります。アプリケーションの起動中、大量のリクエストが流入するため、インタープリタは継続的にハードワークを続けます。

インタプリタが大量の CPU リソースを占有すると、間接的に CPU や負荷などの指標が急上昇し、アプリケーションのパフォーマンスが低下します。これは、アプリケーションのリリース プロセス中に新しく再起動されたアプリケーションで多くのタイムアウトの問題が発生する理由でもあります。

リクエストが増加し続けると、JIT 最適化がトリガーされるため、後続のホットスポット リクエストを解釈して実行する必要がなくなり、JIT 最適化後にキャッシュされたマシン コードが直接実行されます。

✨主な解決策は 2 つあります:✨

1. JIT最適化の効率向上

1 つの方法は、Alibaba によって開発された JDK Dragonwell から学ぶことです。この JDK Dragonwell は、JwarmUp テクノロジーなど、OpenJDK と比較していくつかの独自の機能を提供します。この技術は合格しますJava アプリケーションを最後に実行したときのコンパイル情報をファイルに記録し、次回アプリケーションを起動するときにそのファイルを読み取ることで、クラスのロード、初期化、メソッドのコンパイルを事前に完了し、解釈段階をスキップして、コンパイルされたマシンコードを直接実行します。

2. 瞬間的なリクエスト量を削減する

アプリケーションが起動したばかりのときは、ロード バランシングを調整することでトラフィックが徐々に増加するため、アプリケーションはトラフィックが少ない状態で JIT 最適化をトリガーし、最適化が完了するとトラフィックを徐々に増加させます。

このアプローチは、キャ​​ッシュのウォームアップの考え方に似ています。アプリケーションが開始されたばかりのときは、すぐに大量のトラフィックをアプリケーションに分散せず、最初にトラフィックのごく一部を割り当て、トラフィックのこの部分を通じて JIT 最適化をトリガーします。最適化が完了したら、トラフィックを徐々に増加させます。

おすすめ

転載: blog.csdn.net/qq_39939541/article/details/131778650