909422229__Javaごみの回収コストを削減する5つの提案


GCの低コストを維持するコツは何ですか?
遅延が続くJava 9では、G 1(「Garbage First」)ゴミ回収器がHotSpot仮想マシンのデフォルトのゴミ回収器になります.Serialゴミ回収器からCMS収集器まで、JVMは多くのGC実装を目撃し、G 1は次世代ゴミ回収器となる.
ごみ収集器の発展に伴い、各世代GCは前世代に比べて大きな進歩と改善をもたらした.parallel GCはserial GCに比べて、ゴミ収集器をマルチスレッドで動作させ、マルチコアコンピュータの計算能力を十分に利用しています.CMS(「Concurrent Mark-Sweep」)コレクタはparallel GCと比較して回収プロセスを複数の段階に分けており、アプリケーションスレッドが実行中の場合、収集作業を同時に完了させることができ、「stop-the-world」を頻繁に実行する場合を大幅に改善しています.G 1は、大量のスタックメモリを有するJVMに対してより優れた性能を示し、より予測可能で統一的な一時停止プロセスを有する.
Tip#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;
}

This method allocates a new array, then fills it up with items from another list, only in reverse order. この方法は新しい配列を割り当て,その後,別のlist中の要素で配列を充填したが,要素の数順が変化しただけであった.
この処理では、新しいlistに要素を追加する行のコードに最適化された点で、パフォーマンスのコストがかかる可能性があります.要素を追加するたびにlistは、新しい要素を収容するために下位配列に十分な位置があることを確認する必要があります.空き位置がある場合は、新しい要素を次の空きスロットに簡単に格納するだけです.ない場合は、新しい下位配列を割り当て、古い配列の内容を新しい配列にコピーし、新しい要素を追加します.これにより、複数の配列が割り当てられ、残りの古い配列は最終的に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()個の要素は、初期化時に割り当ての実行を完了し、リストが反復中にメモリを再割り当てする必要がないことを意味する.
Guavaの集合クラスはさらに進み,集合を初期化する際に所望の要素の個数を明確に指定したり,予測値を指定したりすることができる.
List result = Lists.newArrayListWithCapacity(list.size());
List result = Lists.newArrayListWithExpectedSize(list.size());

上記のコードでは、前者は、集合がどれだけの要素を格納するかを正確に知るために使用され、後者の割り当て方法は、誤った見積りを考慮しています.
Tip#2:データストリームの直接処理
次のコードは、データ・ストリームを処理するときに、ファイルからデータを読み込むか、ネットワークからデータをダウンロードするなど、非常に一般的です.
byte[] fileData = readFileToByteArray(new File("myfile.txt"));

生成されたバイト配列は、XMLドキュメント、JSONオブジェクト、またはプロトコルバッファメッセージ、およびいくつかの一般的なオプションを解析することができる.
大きなファイルやファイルのサイズが予測できない場合、JVMが真のファイルを処理するためにバッファを割り当てることができない場合、OutOfMemeoryErrorsが発生するため、上記の方法は賢明ではありません.
データのサイズが管理可能であっても、ファイルデータを格納するためにスタックに非常に大きな領域が割り当てられているため、ゴミ回収時に上記のモードを使用すると、大きなオーバーヘッドが発生します.
1つのより良い処理方法は、適切なInputStream(例えば、この例ではFileInputStreamを使用する)を使用して解析器に直接渡し、ファイル全体を1バイト配列に一度に読み込まないことです.すべての主流のオープンソースライブラリは、次のような入力ストリームを直接受け入れるための対応するAPIを提供します.
FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

Tip#3:可変オブジェクトの使用
不変性には多くのメリットがあります.私が何も言う必要はありません.しかし、ごみ回収に影響を及ぼす利点があるので、注目すべきである.
可変オブジェクトのプロパティは、オブジェクトが作成された後は変更できません(ここでは、データ型を参照するプロパティを使用します).たとえば、次のようになります.
public class ObjectPair {
 
    private final Object first;
    private final Object second;
 
    public ObjectPair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
 
    public Object getFirst() {
        return first;
    }
 
    public Object getSecond() {
        return second;
    }
 
}

上のクラスをインスタンス化すると、可変オブジェクトが生成されます.そのすべてのプロパティはfinalで修飾され、構造が完了すると変更できません.
不変性とは、1つの可変コンテナによって参照されるすべてのオブジェクトが、コンテナ構造が完了する前にオブジェクトが作成されていることを意味します.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を格納するmapを作成するには、このような処理が素晴らしい.
本当の問題は、listストレージintタイプを使用する場合、mapストレージdoubleタイプをvalueとして使用する場合に発生します.汎用型は生データ型をサポートしていないため,もう1つの選択肢はパッケージ型を用いて置き換えることであり,ここではリストを用いる.
この処理は、1つのIntegerが完全なオブジェクトであり、1つのオブジェクトのヘッダが12バイト、およびその内部の維持されたint属性を占有し、各Integerオブジェクトが合計16バイトを占有するため、非常に浪費される.これは、同じ数のintタイプを格納するlistよりも、消費するスペースが4倍になります!これよりも深刻な問題は、実際にはIntegerが真のオブジェクトインスタンスであるため、ゴミ収集フェーズがゴミ収集器によって回収されるかどうかを考慮する必要があるということです.
この問題に対処するために,Takipiでは非常に素晴らしいTrove集合ライブラリを使用した.Troveは、メモリをより効率的に使用する特定のオリジナルタイプの集合をサポートするために、一部の汎用タイプの特定を捨てた.たとえば、パフォーマンスを非常に消費するMapを使用すると、TroveにはTIntDoubleMapという形式の別の特別な選択肢があります.
TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...

Troveの下位実装では、元のタイプの配列が使用されるため、操作セットのときに要素の箱詰め(int->Integer)やコンテナの取り外し(Integer->int)は発生しません.これは、下位が元のデータ型ストレージを使用するため、オブジェクトが格納されていないためです.
最後に
ゴミ収集器の継続的な改善に伴い、運転時の最適化とJITコンパイラもますますスマートになっています.我々は開発者としてGCフレンドリーなコードをどのように書くかをますます少なく考えていることを発見します.しかし、現在の段階では、G 1がどのように改善されても、JVMのパフォーマンスを向上させるためにできることはたくさんあります.