Javaのパフォーマンスを向上させることができるJITの詳細な分析

私がJavaで働き始めたとき、C ++の人々がJavaを書いた人々を軽蔑しているとよく耳にしました。また、一部のグループでは、2つの派閥の人々がしばしば互いに争っているのを見ました。Javaの人気は、クロスプラットフォームや言語に依存しないことと切り離せません。Java、Python、Goのいずれを使用する場合でも、対応する標準のバイトコードファイルになる限り、JVMはそれを認識して実行できます。しかし、Javaが当時のC++からの不満は、主にJavaが 遅いためでしたが 、なぜそう言われたのでしょうか。

作成したプログラムはJVMで認識できますが、マシンでは認識できません。プログラムを実行するには、マシンがプログラムを認識する必要があります。したがって、JVMには、  プログラムをマシンに変換するインタープリターも必要です。認識済み以下に示すように、命令を実行してから実行します

Javaのパフォーマンスを向上させることができるJITの詳細な分析

長時間実行されるJavaプロセスの場合、 インタプリタはプログラムを実行するたびに実行するマシン命令に変換するため、効率があまり良くないため、Javaが遅い と不満を言う  ので、この問題を解決するために、  JIT登場 。

ホットコードの一部のコード(多くの場合、forループで実行)の場合、実行時に、JVMはこれらのコードをマシンで実行できるマシンコードにコンパイルしてキャッシュするため、次回コードを実行するときに、インタプリタによってコンパイルされる必要が あり、マシンはこのプログラムを直接実行してパフォーマンスを向上させることができます。これは、 ジャストインタイムコンパイラ または 略してJITコンパイラと呼ばれます。

JITの種類

Javaのパフォーマンスを向上させることができるJITの詳細な分析

JDK1.8のHotSpot仮想マシンには、C1コンパイラとC2コンパイラの2つの組み込みJITがあります。

C1コンパイラ:シンプルで高速なコンパイラです。主な焦点はローカル最適化です。実行時間が短いプログラムや起動パフォーマンスの要件があるプログラムに適しています。C1コンパイラはコードをほとんど最適化しません。

C2コンパイラ:長時間実行されるサーバー側アプリケーションのパフォーマンスチューニングを実行するコンパイラです。実行時間が長いプログラムや最高のパフォーマンスが必要なプログラムに適しています。その適応性に応じて、この種のジャストインタイムコンパイルは次のようになります。サーバーコンパイラとも呼ばれますが、C2コードは非常に複雑で保守されていないため、C2の代わりにJavaで記述されたGraalコンパイラが開発されます。

ホットコード

JITは、いくつかのホットコードをマシンで認識できるマシンコードにコンパイルしてから、頻繁に呼び出されるコードなどのキャッシュし、forループでコードを挿入します。それでは、JITはどのホットコードであるかをどのように識別しますか?

一部のホットコードがキャッシュされていると言われているのに、すべてではないのはなぜですか?キャッシュには保存するスペースが必要なため、次のコマンドでキャッシュのサイズを確認できます

java -XX:+PrintFlagsFinal –version
复制代码

Javaのパフォーマンスを向上させることができるJITの詳細な分析

JVMは、キャッシュのサイズを制限するためのパラメーター -XX:ReservedCodeCacheSizeも提供します 。スペースがいっぱいになると、JITはコンパイルを続行できず、コンパイルと実行はインタープリター実行になり、プログラムはインタープリターを介して実行されます。

ホットスポットの検出:

ホットスポット検出とは、それらのホットスポットコードをチェックアウトしてコンパイルすることです。ホットスポット検出は、カウンターベースのホットスポット検出です。つまり、各メソッドが呼び出された回数がカウントされます。回数がしきい値に達すると、ホットスポットコードと見なされます。

仮想マシンは、メソッドごとに メソッド呼び出しカウンター と リコールカウンターの2つのカウンターを用意 します。JVMの実行パラメーターを決定した後、これら2つのカウンターには独自のしきい値があります。しきい値に達すると、JITコンパイルが開始されます。

  • メソッド呼び出しカウンター:メソッドが呼び出された回数をカウントするために使用されます。デフォルトはクライアントモードで1500回、サーバーモードでデフォルトで10000回です(デフォルトではすべてサーバーモードを使用します)。次のコマンドを使用して見る
java -XX:+PrintFlagsFinal –version
复制代码

Javaのパフォーマンスを向上させることができるJITの詳細な分析

  • バックサイドカウンター:メソッドでループ本体コードが実行された回数をカウントするために使用されます。バイトコードで制御フローの逆方向ジャンプが発生した命令は バックサイドと呼ば れます。サーバーモードでは、デフォルトで10700回の値になります。 。
在JVM内存结构中是有一个程序计数器的,它表示的是字节码需要你执行的下一行指令的行号,当我们执行
第一次循环之后,程序又回调到for循环的第一行执行,这个就叫回边
for(int i = 0; i < 10000; i ++) {
     int a = i;
     int b = a + i;
}
复制代码

JITがJavaパフォーマンスを最適化する方法

メソッドのインライン化

メソッドのインライン化の最適化とは、呼び出されたメソッドのコードを呼び出し元のメソッドにコピーして、実際のメソッド呼び出しを回避することです。

方法add1()要计算四个数加起来的和,然后又调用了 add2()方法,其实根本没必要有 add2()这个方法
所以JIT会进行优化
public int add1(int a,int b,int c,int d) {
    return add2(a,b) + add2(c,d);
}

public int add2(int a,int b) {
    return a + b;
}

优化后就一个方法就行了,也就是把 add2()方法要执行的代码直接复制到 add1()方法中执行,就不要去
调用 add2()这个方法了,这样就不存在方法调用,就一个方法就可以了
public int add1(int a,int b,int c,int d) {
    return a + b + c + d;
}
复制代码

为什么方法内联可以优化Java性能呢?我们知道一个方法的执行在JVM内存结构中虚拟机栈对应的就是入栈,方法结束就对应着出栈,出栈和入栈都是有性能消耗的,所以少一个方法执行就减少了一次对应的出栈和入栈,性能也就能够提升

锁消除

在非线程安全情况下,我们都会使用线程安全的容器,举个例子,比如字符串拼接的StringBuffer和StringBuilder,StringBuffer的方法被关键字synchornized修饰,所以性能会比StringBuilder差,但是在局部方法中二者的性能确实差不多的,因为在局部方法中是单线程访问的,不存在线程安全问题,

// jdk8默认情况下开启了锁消除
public static void main(String[] args) {
    long start_sb = System.currentTimeMillis();
    for(int i = 0; i < 10000000; i ++) {
        SBuilder("king","coco");
    }
    long end_sb = System.currentTimeMillis();
    System.out.println("StringBuilder话费的时间:" + (end_sb - start_sb));


    long start_sf = System.currentTimeMillis();
    for(int i = 0; i < 10000000; i ++) {
        SBuffer("king","coco");
    }
    long end_sf = System.currentTimeMillis();
    System.out.println("StringBuffer话费的时间:" + (end_sf - start_sf));
}


public static void SBuilder(String str1,String str2) {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(str1);
    stringBuilder.append(str2);
}

public static void SBuffer(String str1,String str2) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str1);
    stringBuffer.append(str2);
}
复制代码

花费的时间差不多

Javaのパフォーマンスを向上させることができるJITの詳細な分析

然后我们可以通过启动参数 -XX:-EliminateLocks 关闭锁消除, -XX:+EliminateLocks 开启锁消除

Javaのパフォーマンスを向上させることができるJITの詳細な分析

Javaのパフォーマンスを向上させることができるJITの詳細な分析

关闭了锁消除之后,StringBuffer所话费的时间明显增加了很多,性能降低了,JIT在编译的时候发现如果使用了线程安全的容器,比如StringBUffer,但是发现程序不会存在线程并发问题,就会执行锁消除来提高程序的性能

逃逸分析

Java创建的大部分对象都是在堆中的,而不是全部的对象。逃逸分析技术就是在创建对象的时候判断这个对象是在保存在堆中还是保存在栈中,那么保存在栈中有什么好处呢??

首先说说保存在堆中吧,JVM垃圾收集的主要对象就是堆,创建的对象在堆中保存,那么当你这个对象不用 的时候就要被回收,我们知道垃圾回收是会消耗一定的性能的,但是如果你这个对象经过逃逸分析之后,发现这个对象可以在栈中分配,那么当你这个方法结束之后,也就是出栈,那么该对象自然就没了,也不需要垃圾收集器回收,这样就减少了垃圾收集器的工作,性能自然就能提升了

那么什么是逃逸分析呢?

我们创建了一个StringBuilder对象,但是这个对象只有在这个方法内部有效,该对象没有被返回出去
也就是说没有方法需要a()方法创建的这个StringBuilder对象,所以这个StringBuilder对象不会发生
逃逸
public String a() {
  StringBuilder sb = new StringBuilder();
  sb.append("123");
  return "123";
}


这个StringBuilder对象就发生逃逸了,因为有其它方法需要b()方法创建的StringBuilder对象
也就是说这个对象发生了逃逸
public StringBuilder b() {
  StringBuilder sb = new StringBuilder();
  sb.append("123");
  return sb;
}

复制代码

逃逸分析性能测试

逃逸分析默认开启

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for(int i = 0; i < 50000000; i ++) {
        createPeople();
    }
    long end = System.currentTimeMillis();
    System.out.println("花费的时间是:" + (end - start));
}

public static void createPeople() {
    People people = new People(10,"coco");
}

static class People{
    Integer age;
    String name;

    public People(Integer age,String name) {
        this.age = age;
        this.name = name;
    }
}
复制代码

添加打印GC收集信息

Javaのパフォーマンスを向上させることができるJITの詳細な分析

Javaのパフォーマンスを向上させることができるJITの詳細な分析

发现这么多对象很快就创建好了,并且没有垃圾收集日志的打印

关闭逃逸分析

Javaのパフォーマンスを向上させることができるJITの詳細な分析

エスケープ分析をオフにすると、費やす時間が大幅に増加します

Javaのパフォーマンスを向上させることができるJITの詳細な分析

そして、ログを印刷するためにGCを傷つける可能性があります

Javaのパフォーマンスを向上させることができるJITの詳細な分析

オブジェクトがすべてヒープ内にあるため、GC収集情報があることがわかり、GCが発生します。逆に、エスケープ分析テクノロジが有効になっているオブジェクトは、スタックの出入りで直接破棄されます。 GCが必要なため、パフォーマンスが向上します。

Javaのパフォーマンスを向上させることができるJITの詳細な分析

おすすめ

転載: juejin.im/post/7120813685727035428
おすすめ