ガベージコレクションのオーバーヘッドを削減する5つの「ハッキング」



この投稿では、ガベージコレクターがメモリの割り当てと解放に費やす時間を短縮するのに役立つコード効率を改善する5つの方法について説明します。 長いガベージコレクションプロセスは、「世界を止める」として知られる現象につながる可能性があります。



一般的な情報



ガベージコレクター(GC)は、存続期間の短いオブジェクトに対する多数のメモリ割り当てを処理するために存在します(たとえば、Webページのレンダリング中に割り当てられたオブジェクトは、ページが表示されるとすぐに古くなってしまいます)。



この場合のGCは、いわゆる「若い世代」-新しいオブジェクトが配置されるヒープのセグメントを使用します。 各オブジェクトには、フィールドの「年齢」(オブジェクトのヘッダーにある「年齢」)があります。このフィールドは、生存したガベージコレクションの数を決定します。 特定の年齢に達するとすぐに、オブジェクトは「古い」世代と呼ばれるヒープの別の領域にコピーされます。



このプロセスはまだ効果的ですが、すでに目に見えてきています。 一時オブジェクトのメモリ割り当て量を削減する機能は、生産性を向上させるのに役立ちます。特に、広範囲に拡張された環境や、リソースが限られているAndroidアプリケーションで役立ちます。



以下の5つの方法は、実際の時間を費やさず、コードの可読性を低下させずに、日常の開発でメモリ操作の効率を向上させるために使用できます。



1.暗黙の文字列の使用を避けます



行は、ほとんどすべてのデータ構造の不可欠な部分です。 他のプリミティブタイプよりも多くのリソースを消費するため、メモリ消費に大きな影響を及ぼします。



行が不変であることを忘れないでください。 割り当て後は変更されません。 文字列を連結するときの「+」などの演算子は、実際に文字列連結を含む新しいStringオブジェクトを作成します。 他のすべてにとって、これはStringBuilderオブジェクトの暗黙的な作成につながり、それはユニオン操作自体を実行します。



以下に例を示します。

a = a + b; // a  b - 
      
      





そして、コンパイラの舞台裏で生成される実際のコードは次のとおりです。

 StringBuilder temp = new StringBuilder(a). temp.append(b); a = temp.toString(); //    . //  “a”   .
      
      





現実はさらに悪い

次の例を考えてみましょう。

 String result = foo() + arg; result += boo(); System.out.println(“result = “ + result);
      
      





ここでは、3つのStringBuildersが暗黙的に選択されています-「+」操作ごとに1つ、追加の行が2つ-1つ目は2番目の割り当ての結果、もう1つはprintlnメソッドに渡されます。 その結果、簡単なコードで5つの追加オブジェクトを受け取りました。



Webページの生成、XMLの操作、ファイルからのテキストの読み取りなど、実際のプログラムで何が起こるかを考えてください。 ループ内の同様のコードは、数百または数千の暗黙的に割り当てられたオブジェクトになります。 VMにはこれに対処するメカニズムがありますが、すべてに価格があり、ユーザーが支払うことになります。



解決策: 1つの方法は、StringBuilderを明示的に作成することです。 次の例では、同じ結果が得られますが、メモリは1つのStringBuilderと最終結果の1つの行にのみ割り当てられます。

 StringBuilder value = new StringBuilder(“result = “); value.append(foo()).append(arg).append(boo()); System.out.println(value);
      
      





このような場合、文字列とStringBuilderが暗黙的に割り当てられることに留意して、頻繁に実行されるコードでの小さなメモリ割り当ての数を大幅に減らすことができます。



2.リストの初期容量を設定する



ArrayListなどの動的に拡張可能なコレクションは、可変長データを保持するための基本的な構造の一部です。 ArrayListおよびその他のコレクション(HashMap、TreeMapなど)は、基礎となる配列Object []を使用して実装されます。 文字列(文字配列のアドオン)と同様に、配列のサイズは変更されません。 明らかな質問は、基礎となる配列のサイズが不変である場合、どのようにコレクションにアイテムを追加するのですか? 答えは明白です-大きな配列を割り当てることによって。



次の例を考えてみましょう。

 List<Item> items = new ArrayList<Item>(); for (int i = 0; i < len; i++) { Item item = readNextItem(); items.add(item); }
      
      





len変数の値は、ループの終了前に処理される要素の最大数を決定します。 それにもかかわらず、この値はArrayListコンストラクターにとって不明であり、コンストラクターはデフォルトのサイズで配列を割り当てざるを得ません。 内部アレイの容量が超過すると、十分な長さの新しいアレイに置き換えられ、その結果、前のアレイがゴミになります。



ループが1000回実行されると、新しい配列の複数の割り当てと古い配列のアセンブリが発生する可能性があります。 高度にスケーラブルな環境で実行されているプログラムの場合、これらの(de)割り当てはプロセッササイクルから差し引かれて使用できません。



解決策:可能な限り初期容量を示します。

 List<MyObject> items = new ArrayList<MyObject>(len);
      
      





これにより、実行中に発生する内部配列の不要なメモリ割り当てがなくなります。 正確なサイズがわからない場合は、おおよそまたは予想される平均値を指定する価値があり、予期しないオーバーフローが発生した場合に上から数パーセント追加します。



3.プリミティブ型の効率的なコレクションを使用する



Javaコンパイラの現在のバージョンは、「オートボクシング」の使用により、GCで選択および削除できる標準オブジェクトでプリミティブ値をラップすることにより、プリミティブ型のキーおよび値を持つ通常配列および連想配列をサポートします。



これは負の結果をもたらすことがあります。 Javaでは、ほとんどのコレクションは内部配列を使用して実装されます。 HashMapに追加された各キーと値のペアにより、両方の値を保持するために内部オブジェクトが割り当てられます。 この避けられない悪は、連想配列の使用を伴います-要素がマップに追加されるたびに、これは新しいオブジェクトの割り当てにつながり、おそらく古いオブジェクトのアセンブリにつながります。 超過容量に関連するコストがあります。 新しい内部アレイのリソースの再割り当て。 数千またはそれ以上のオブジェクトを持つ大きな連想配列を扱う場合、これらの内部割り当てはGCに大きな影響を与える可能性があります。



一般的なケースは、プリミティブ型(たとえば、識別子)とオブジェクト間のある種のマッピングを保存することです。 HashMapはオブジェクト型を格納するように設計されているため、これは、各挿入がプリミティブ型の値を「パッケージ化」する別のオブジェクトの作成を意味することを意味します。



標準のInteger.valueOf()メソッドは-128〜127の値をキャッシュしますが、この範囲外の数値は各キーと値のペアに個別のオブジェクトを割り当てます。 これにより、各連想配列で3倍の GCオーバーヘッドが発生します。 C ++から来た人にとって、これはニュースかもしれません-STLのテンプレートのおかげで、この問題は非常に効果的に解決されました。



幸いなことに、Javaの新しいバージョンがこれに取り組んでいます。 それまでの間、プリミティブ型ツリー、連想配列、リストを提供する素晴らしいサードパーティライブラリの助けを借りて、何らかの方法で効率を改善しようとします。 Troveを強くお勧めします。Troveと一緒にかなりの時間をかけ、重要なコードでのガベージコレクションのオーバーヘッドの実際の削減を確認できます。



4.メモリ内のバッファの代わりにストリームを使用する



サーバーアプリケーションで操作するデータのほとんどは、ネットワーク接続またはデータベースからのファイルまたはデータストリームの形式で送られてきます。 ほとんどの場合、着信データはシリアル化された形式で提供され、オブジェクトに対して操作を実行する前にオブジェクトへの逆シリアル化が必要です。 この段階では、多くの場合かなりの量の暗黙的なメモリ消費が発生します。



通常、データはByteArrayInputStream、ByteBuffeを使用してメモリに読み込まれ、結果が逆シリアル化に渡されます。



これは悪いアプローチかもしれません 最初にデータを割り当ててから、データ用のスペースを解放する必要がありますが、それはそれらからのオブジェクトの構築の最後に限られます。 ただし、原則として、データのサイズは不明であるため、ご想像のとおり、バイト[]配列に一定のメモリが割り当てられ、バッファ容量を超えるとサイズが大きくなります。



ソリューションは非常に簡単です。 ネイティブJavaシリアライザー、プロトコルバッファーなど、多くのライブラリー ネットワークストリームから直接データを使用して、逆シリアル化されたオブジェクトを構築できます。 メモリや内部アレイにデータを保存する必要はありません。 可能な限りこのアプローチを使用してください-GCは感謝の意を表します。



5.免疫は常に良いとは限りません



免疫は優れたものですが、高性能コンピューティングの場合には重大な欠点になる可能性があります。 給与計算オブジェクトのメソッド間の転送シナリオを検討してください。



関数からコレクションを返す場合は、通常、メソッド内にコレクションオブジェクト(ArrayListなど)を作成し、それを入力して、コレクション不変インターフェイスの形式で返すことをお勧めします。

しかし、場合によってはこれは受け入れられません 。 たとえば、メソッドから返されたコレクションが最終的なコレクションにアセンブルされる場合。 不変性は透明性を提供しますが、負荷の高いサービス状況では、これは中間コレクションに対する大量のメモリ割り当てを意味します。



この場合の解決策は、メソッドから新しいコレクションを返すことを避け、代わりに結果のコレクションのオブジェクトをメソッドのパラメーターとして渡すことです。



例1.(無効)

 List<Item> items = new ArrayList<Item>(); for (FileData fileData : fileDatas) { //       // , , -   items.addAll(readFileItem(fileData)); }
      
      





例2

 List<Item> items = new ArrayList<Item>( fileDatas.size() * avgFileDataSize * 1.5); for (FileData fileData : fileDatas) { readFileItem(fileData, items); //    }
      
      





例2は免責の規則を無視していますが(これは一般的な状況で推奨されます)、多くのサイド割り当てを回避することができました。これは、集中的な計算の場合にGCに非常に良い影響を与えます。



他に読むものは何ですか?



1) ストリングインターンについて



2) 効果的なラッパーについて



3) Troveについて



4) Trove onHabréについて



All Articles