ホームページ  >  記事  >  Java  >  Java ガベージ コレクションのオーバーヘッドを削減する 5 つのヒント

Java ガベージ コレクションのオーバーヘッドを削減する 5 つのヒント

伊谢尔伦
伊谢尔伦オリジナル
2016-12-05 10:59:541285ブラウズ

GC オーバーヘッドを低く保つためのヒントは何ですか?

Java 9 のリリースが遅れたことにより、G1 (「ガベージ ファースト」) ガベージ コレクターが HotSpot 仮想マシンのデフォルトのガベージ コレクターになります。シリアル ガベージ コレクターから CMS コレクターまで、JVM は多くの GC 実装を目撃しており、G1 はその次世代のガベージ コレクターになります。

ガベージ コレクターの開発により、GC の各世代は前世代と比較して大きな進歩と改善をもたらしました。シリアル GC と比較して、パラレル GC では、ガベージ コレクターがマルチスレッド方式で動作し、マルチコア コンピューターの計算能力を最大限に活用できます。並列 GC と比較して、CMS (「同時マークスイープ」) コレクターはリサイクル プロセスを複数の段階に分割するため、アプリケーション スレッドの実行中に収集作業を同時に完了でき、「停止」の頻繁な実行が大幅に改善されます。 -世界」の状況。 G1 は、大量のヒープ メモリを備えた JVM のパフォーマンスが向上し、より予測可能で均一な一時停止プロセスを備えています。

ヒント #1: コレクション容量を予測する

カスタムおよび拡張実装 (Trove や Google の Guava など) を含むすべての標準 Java コレクションは、内部で配列 (ネイティブ データ型またはオブジェクトベースの型のいずれか) を使用します。配列が一度割り当てられると、そのサイズは不変であるため、コレクションに要素を追加すると、ほとんどの場合、古い配列 (コレクションの基礎となる実装)。

コレクション初期化のサイズが指定されていない場合でも、ほとんどのコレクション実装は、配列の再割り当て処理を最適化し、そのオーバーヘッドを最小限に抑えようとします。ただし、コレクションを構築するときにサイズを指定すると、最良の結果が得られます。

簡単な例として次のコードを分析してみましょう:

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList();
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}

このメソッドは、新しい配列を割り当て、それから別のリストの項目を逆の順序でのみ埋めます。次に、その配列に別のリストの要素を埋めます。要素の番号順が変わります。

この処理方法では、パフォーマンスに大きな代償を払う可能性があります。最適化ポイントは、新しいリストに要素を追加するコード行です。 各要素が追加されると、リストはその基になる配列に新しい要素を収容するのに十分なスペースがあることを確認する必要があります。空きスロットがある場合、新しい要素は次の空きスロットに単純に格納されます。そうでない場合は、新しい基になる配列が割り当てられ、古い配列の内容が新しい配列にコピーされ、新しい要素が追加されます。これにより、配列が複数回割り当てられることになり、残った古い配列は最終的に GC によって回収されます。

コレクションを構築するときに、基礎となる配列に格納する要素の数を知らせることで、これらの冗長な割り当てを回避できます

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList(list.size());
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}

上記のコードは、ArrayList 要素のコンストラクターを通じて list.size() を格納するのに十分な大きさのスペースを指定します。割り当ては初期化中に完了します。つまり、List は反復中にメモリを再度割り当てる必要がありません。

Guava のコレクション クラスはさらに一歩進んで、コレクションの初期化時に予期される要素の数を明示的に指定したり、予測値を指定したりすることができます。

List result = Lists.newArrayListWithCapacity(list.size());List result = Lists.newArrayListWithExpectedSize(list.size());

上記のコードでは、前者はコレクションに格納される要素の数がすでに正確にわかっている場合に使用され、後者は誤った推定を考慮した方法で割り当てられます。

ヒント #2: データ ストリームを直接処理する

ファイルからのデータの読み取りやネットワークからのデータのダウンロードなど、データ ストリームを処理する場合、次のコードが非常に一般的です:

byte[] fileData = readFileToByteArray(new File("myfile.txt"));

結果のバイト配列は、解析された XML ドキュメントである可能性があります。 、JSON オブジェクトまたはプロトコルのバッファリングされたメッセージと、いくつかの一般的なオプション。

大きなファイルや予測不可能なサイズのファイルを扱う場合、JVM が実際のファイルを処理するためのバッファを割り当てることができない場合、OutOfMemoryErrors が発生するため、上記のアプローチは非常に賢明ではありません。

たとえデータのサイズが管理可能な場合でも、上記のパターンを使用すると、ファイル データを保存するためにヒープ内に非常に大きな領域が割り当てられるため、ガベージ コレクションに関しては依然として大きなオーバーヘッドが発生します。

より良い方法は、ファイル全体を一度にバイト配列に読み取るのではなく、適切な InputStream (この例では FileInputStream など) を使用してパーサーに直接渡すことです。すべての主流のオープン ソース ライブラリは、次のような、入力ストリームを直接受け入れるための対応する API を提供しています。詳細に入る必要すらありません。ただし、ガベージ コレクションに影響を与える注目すべき利点があります。

不変オブジェクトの属性は、オブジェクトの作成後に変更することはできません (この例では参照データ型の属性を使用しています)。たとえば、次のとおりです。

FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

上記のクラスをインスタンス化した後、不変オブジェクトが生成されます - そのすべての属性は、final で変更され、構築完了後に変更することはできません。

不可变性意味着所有被一个不可变容器所引用的对象,在容器构造完成前对象就已经被创建。就 GC 而言:这个容器年轻程度至少和其所持有的最年轻的引用一样。这意味着当在年轻代执行垃圾回收的过程中,GC 因为不可变对象处于老年代而跳过它们,直到确定这些不可变对象在老年代中不被任何对象所引用时,才完成对它们的回收。

更少的扫描对象意味着对内存页更少的扫描,越少的扫描内存页就意味着更短的 GC 生命周期,也意味着更短的 GC 暂停和更好的总吞吐量。

Tip #4: 小心字符串拼接

字符串可能是在所有基于 JVM 应用程序中最常用的非原生数据结构。然而,由于其隐式地开销负担和简便的使用,非常容易成为占用大量内存的罪归祸首。

这个问题很明显不在于字符串字面值,而是在运行时分配内存初始化产生的。让我们快速看一下动态构建字符串的例子:

public static String toString(T[] array) {    
   String result = "[";    
   for (int i = 0; i & lt; array.length; i++) {
        result += (array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            result += ", ";
        }
    }
    result += "]";
    return result;
}

这是个看似不错的方法,接收一个字符数组然后返回一个字符串。但是这对于对象内存分配却是灾难性的。

很难看清这语法糖的背后,但是幕后的实际情况是这样的:

public static String toString(T[] array) {    
     String result = "[";    
     for (int i = 0; i & lt; array.length; i++) {
        StringBuilder sb1 = new StringBuilder(result);
        sb1.append(array[i] == array ? "this" : array[i]);
        result = sb1.toString();        
        
        if (i & lt; array.length - 1) {
            StringBuilder sb2 = new StringBuilder(result);
            sb2.append(", ");
            result = sb2.toString();
        }
    }
    StringBuilder sb3 = new StringBuilder(result);
    sb3.append("]");
    result = sb3.toString();
    return result;
}

字符串是不可变的,这意味着每发生一次拼接时,它们本身不会被修改,而是依次分配新的字符串。此外,编译器使用了标准的 StringBuilder 类来执行这些拼接操作。这就会有问题了,因为每一次迭代,既隐式地分配了一个临时字符串,又隐式分配了一个临时的 StringBuilder 对象来帮助构建最终的结果。

最佳的方式是避免上面的情况,使用 StringBuilder 和直接的追加,以取代本地拼接操作符(“+”)。下面是一个例子:

public static String toString(T[] array) {
    StringBuilder sb = new StringBuilder("[");    
    for (int i = 0; i & lt; array.length; i++) {
        sb.append(array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            sb.append(", ");
        }
    }
    sb.append("]");    
    return sb.toString();
}

这里,我们只在方法开始的时候分配了唯一的一个 StringBuilder。至此,所有的字符串和 list 中的元素都被追加到单独的一个StringBuilder中。最终使用 toString() 方法一次性将其转成成字符串返回。

Tip #5: 使用特定的原生类型的集合

Java 标准的集合库简单且支持泛型,允许在使用集合时对类型进行半静态地绑定。比如想要创建一个只存放字符串的 Set 或者存储 Map

TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);...

Trove 的底层实现使用了原生类型的数组,所以当操作集合的时候不会发生元素的装箱(int->Integer)或者拆箱(Integer->int), 没有存储对象,因为底层使用原生数据类型存储。

最后

随着垃圾收集器持续的改进,以及运行时的优化和 JIT 编译器也变得越来越智能。我们作为开发者将会发现越来越少地考虑如何编写 GC 友好的代码。然而,就目前阶段,不论 G1 如何改进,我们仍然有很多可以做的事来帮 JVM 提升性能。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。