JVM仮想マシンのコンパイル最適化

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して18日目です。クリックしてイベントの詳細をご覧ください

この記事は主に概念の章であり、コンパイルの最適化のためのJVMのいくつかのコア概念を要約および抽出します。ホットスポット検出、メソッドのインライン化、エスケープ分析、共通部分式除去、配列境界の除去などです。

ホットスポットの検出

仮想マシンは、メソッドまたはコードブロックが非常に頻繁に実行されていることを検出すると、これらのコードを「ホットスポットコード」として識別します。

コードがホットコードであるかどうかを判断するこの動作は「ホットスポット検出」と呼ばれ、ホットコードを判断する方法は2つあります。

1.サンプルベースのホットスポットコード検出

仮想マシンは、兄のスレッドの呼び出しスタックの最上位を定期的にチェックするために使用されます。メソッドがスタックの最上位に頻繁に表示されることがわかった場合、このメソッドは「ホットメソッド」です。サンプリングベースのホットスポット検出の利点は、シンプルで効率的であり、メソッドの呼び出し関係を簡単に取得できることです(呼び出しスタックを拡張するだけです)。シャドウおよび暴動のホットスポット検出。

2.カウンターベースのホットスポットコード検出

このメソッドを使用する仮想マシンは、各メソッド(コードブロックも含む)のカウンターを確立し、メソッドの実行時間をカウントし、実行時間が特定のしきい値を超えた場合に「ホットメソッド」と見なします。この統計的手法は、実装が面倒であり、メソッドごとにカウンターを設定して維持する必要があり、メソッドの呼び出し関係を直接取得することはできません。しかし、その統計結果は比較的正確で厳密です。

HotSpot仮想マシンは、カウンターベースのホットスポット検出方法を使用します。カウンターには次の2つのタイプがあります。

  • メソッド呼び出しカウンター(呼び出しカウンター)、サーバーモードではデフォルトで10000回
  • バックエッジカウンター、サーバーモードでデフォルトで10700回

メソッドのインライン化

メソッド制限の最適化は、実際のメソッド呼び出しを回避するために、ターゲットメソッドのコードを呼び出されたメソッドにコピーすることです。

  • ホットスポット検出テクノロジーがトリガー
  • メソッド本体サイズ制限
  • 使用方法内联提升性能

测试代码:


/**
 * VM Args:
 * -XX:+PrintCompilation          控制台打印编译过程信息
 * -XX:+UnlockDiagnosticVMOptions 解锁对 JVM 进行诊断的选项参数,默认关闭
 * -XX:+PrintInlining             打印方法内联
 * @author Administrator
 */
public class MethodInliningTest {

    public static void main(String[] args) {
        testInline(args);
    }

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

    public static void testInline(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            add(i,i+1);
        }
    }
}

复制代码

查看打印信息,当前代码触发方法内联 image.png

逃逸分析(Escape Analysis)

**逃逸分析(Escape Analysis)**是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。

逃逸分析的基本原理是: 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度

栈上分配(Stack Allocations)

在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是 Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换(Scalar Replacement)

若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java 中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上 (栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

同步消除(Synchronization Elimination

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。

VM 参数:

-XX:+EliminateLocks       开启锁消除(JDK1.8 默认开启)
复制代码

公共子表达式消除(Common Subexpression Elimination)

公共子表达式消除是一个非常经典的、普片应用各种编译器的优化技术。

在同一个作用域下,如果程序中有公共的计算表达式,多次出现参与运算的结果相同,计算结果为 E,那么后面的表达式直接用 E 替代结果。

一个表达式已经被计算过了,并且从先前计算到现在的计算所有的变量值都没有发生变化,称为 E 为公共表达式。对于这种表达式,我们就不需要重新计算了,只需要使用之前计算过的表达式结果替代 E 。如果这种代码优化局限与程序代码块内,可称为局部公共表达式消除(Local Common Subexpression Elimination)。如果这种优化的范围覆盖了多个代码块,那就称为全局公共子表达式消除(Global Common Subexpression Elimination)。

举个例子:

int d = (c * b) * 12 + a + (a + c * b);
复制代码

虚拟机的即时编译器会进行代码优化(因为在代码块内 c、b 的之没有改变,编译器认为没必要多次计算),上面的表达式会被优化为:

int d = E * 12 + a + (a + E);
复制代码

在这个时候编译器也可能会进行代数简化(Algebraic Simplification)。

int d = E * 13 + a + a
复制代码

经过简化后再计算起来就可以节省一些 CPU 的时间。

数组边界检查消除(Array Boounds Checking Elimination)

数组边界检查消除(Array Boounds Checking Elimination)是及时编译器优化的经典技术。对于 Java 而言本来就是一门动态安全的语言,比如我们访问一个数组 foo[i] 的时候:

  1. 先去检查数组是否为空,如果 foo 为空返回 NullPointException异常。
  2. 如果数组越界会得到 ArrayIndexOutOfBoundsExecption异常。

其实 JVM 为我们做了非常多的隐含条件判断。

但是对于这些操作,为了安全肯定是要做的,但是如果对 foo[i] 循环取值,如果编译器通过分析发现可以判定循环变量的取值永远在区间 [0, foo.length] 之内,那么在这个循环中我们就可以吧这个那个数组的上下界限检查消除掉。

举个例子:

if (i >= 0 && i < foo.length) {
    return foo[i];
}
复制代码

优化过后:

return foo[i];
复制代码

除此之外还有一种隐式异常处理,Java 空指针检查和算数运算除数为 0 的检查都采用了这种方案。 举个例子:

if (foo != null) {
    return foo.value;
} else {
    throw new NullPointException();
}
复制代码

在隐式异常优化之后:

try {
    return foo.value
} catch (segment_fault) {
    uncommon_trap();
}
复制代码

仮想マシンは、セグメンテーション違反の信号例外ハンドラーを登録します。fooが空でない場合、値にアクセスする際のオーバーヘッドはありません。ただし、fooが空の場合、例外処理に入ります。これには、ユーザーモードからカーネルモードへのプロセスが含まれ、処理の完了後にユーザーモードに戻ります。同じ操作は、nullチェックよりもはるかに低速です。HptSpotは、実行時に収集された監視情報に基づいて、最適なソリューションを自動的に選択します。

その他の最適化:Autobox Elimination、Safepoint Elimination、Dereflectionなど。

リファレンスドキュメント

  • 「JVM仮想マシンの深い理解」ZhouZhiming

おすすめ

転載: juejin.im/post/7087956428475858974