web-dev-qa-db-ja.com

ラージオブジェクトヒープフラグメンテーション

私が取り組んでいるC#/。NETアプリケーションは、遅いメモリリークに悩まされています。私はCDBをSOSとともに使用して、何が起こっているのかを判断しようとしましたが、データは意味をなさないようです。

アプリケーションは64ビットフレームワークで実行されています。データを継続的に計算し、リモートホストにシリアル化し、ラージオブジェクトヒープ(LOH)をかなりの割合でヒットしています。ただし、一時的なLOHオブジェクトのほとんどは一時的なものです。計算が完了し、リモートホストに送信されたら、メモリを解放する必要があります。しかし、私が見ているのは、メモリの空きブロックにインターリーブされた多数の(ライブ)オブジェクト配列です。たとえば、LOHからランダムセグメントを取得します。

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

私のアプリケーションが各計算中に長寿命の大きなオブジェクトを作成している場合、これが当てはまることは明らかです。 (これを行い、ある程度のLOHフラグメンテーションがあることを受け入れますが、それはここでは問題ではありません。)問題は、上記のダンプで見ることができる非常に小さい(1056バイト)オブジェクト配列であり、コードでは見ることができません作成され、何とか根付いたままです。

また、ヒープセグメントがダンプされるときにCDBがタイプを報告しないことに注意してください。これが関連しているかどうかはわかりません。マークされた(<-)オブジェクトをダンプすると、CDB/SOSはそれを正常に報告します。

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

オブジェクト配列の要素はすべて文字列であり、文字列はアプリケーションコードから認識できます。

また、!GCRootコマンドがハングして戻ってこないので、GCルートを見つけることができません(一晩残してみました)。

だから、これらの小さな(<85k)オブジェクト配列がLOHで終わる理由について誰かが光を当てることができれば、とても感謝しています:.NETはどのような状況で小さなオブジェクト配列をそこに置くのでしょうか?また、これらのオブジェクトのルートを確認する別の方法を知っている人はいますか?


アップデート1

昨日私が思いついたもう1つの理論は、これらのオブジェクト配列は大きくなり始めたが、メモリダンプに明らかな空きメモリのブロックを残して縮小されたというものです。私が疑っているのは、オブジェクト配列が常に1056バイト(128要素)、参照用の128 * 8、および32バイトのオーバーヘッドであるように見えることです。

考えは、おそらくライブラリまたはCLRの一部の安全でないコードが配列ヘッダーの要素数フィールドを破損しているということです。私が知っているロングショットのビット...


更新2

ブライアン・ラスムッセンのおかげで(受け入れられた答えを参照)、問題はストリングインターンテーブルによって引き起こされるLOHの断片化として特定されました!これを確認する簡単なテストアプリケーションを作成しました。

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

アプリケーションは、最初にループ内で一意の文字列を作成および間接参照します。これは、このシナリオでメモリがリークしないことを証明するためです。当然、そうすべきではありません。

2番目のループでは、一意の文字列が作成され、インターンされます。このアクションは、インターンテーブルをルートにします。私が気づかなかったのは、インターンテーブルがどのように表されるかです。 LOHで作成された一連のページ(128文字列要素のオブジェクト配列)で構成されているようです。これは、CDB/SOSでより明白です。

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

LOHセグメントのダンプを取得すると、リークしているアプリケーションで見たパターンが明らかになります。

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

ワークステーションは32ビットで、アプリケーションサーバーは64ビットであるため、オブジェクト配列サイズは(1056ではなく)528です。オブジェクト配列の長さは依然として128要素です。

したがって、この物語の教訓は、非常に慎重なインターンであるということです。インターンしている文字列が有限集合のメンバーであることがわからない場合、少なくともバージョン2のCLRでは、LOHの断片化によりアプリケーションがリークします。

このアプリケーションの場合、非整列化中にエンティティ識別子をインターンする一般的なコードが逆シリアル化コードパスにあります。これが原因であると強く疑います。ただし、開発者の意図は、同じエンティティが複数回デシリアライズされた場合に、識別子文字列の1つのインスタンスのみがメモリに保持されるようにしたかったため、明らかに良かったです。

96
Paul Ruane

CLRはLOHを使用して、いくつかのオブジェクトを事前に割り当てます( インターンされた文字列に使用される配列 など)。これらの一部は85000バイト未満であるため、通常はLOHに割り当てられません。

これは実装の詳細ですが、この理由は、プロセスがそれ自体である限り存続するはずのインスタンスの不要なガベージコレクションを避けるためだと思います。

また、やや難解な最適化のため、すべてのdouble[]の1000以上の要素もLOHに割り当てられます。

45
Brian Rasmussen

.NET Framework 4.5.1には、ガベージコレクション中にラージオブジェクトヒープ(LOH)を明示的に圧縮する機能があります。

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

詳細は GCSettings.LargeObjectHeapCompactionMode をご覧ください

12
Andre Abrantes

GCがどのように機能するかの説明、および寿命の長いオブジェクトが第2世代で終わる部分、およびLOHオブジェクトのコレクションが完全なコレクションでのみ発生する場合-第2世代のコレクションと同様に、思い浮かぶのはアイデアです。 ..ジェネレーション2と大きなオブジェクトを一緒に収集しないので、同じヒープ内に保持しないのはなぜですか?

それが実際に発生する場合、小さなオブジェクトがLOHと同じ場所に配置される方法を説明します-それらが第2世代で終了するのに十分な長寿命である場合。

そして、あなたの問題は、私に起こるアイデアに対するかなり良い反論のように見えます-それはLOHの断片化をもたらすでしょう。

要約:あなたの問題couldは、同じヒープ領域を共有するLOHとジェネレーション2によって説明できますが、これが説明であることの証明ではありません。

更新:!dumpheap -statの出力は、この理論を大いに吹き飛ばします!ジェネレーション2とLOHには独自の領域があります。

2

[〜#〜] loh [〜#〜] 割り当ての正確な call-stack を特定する方法がいくつかあります。

また、LOHフラグメンテーションを回避するには、オブジェクトの大きな配列を事前に割り当てて固定します。必要なときにこれらのオブジェクトを再利用します。 LOHフラグメンテーションの post です。このようなものは、LOHフラグメンテーションを回避するのに役立ちます。

1
Naveen

形式がアプリケーションとして認識できる場合、この文字列形式を生成しているコードを特定していないのはなぜですか?いくつかの可能性がある場合は、一意のデータを追加して、どのコードパスが原因であるかを調べてください。

配列が大きな解放されたアイテムでインターリーブされているという事実は、それらが元々ペアになっているか、少なくとも関連していると推測します。解放されたオブジェクトを識別して、それらが生成されたものと関連する文字列を把握してください。

これらの文字列を生成しているものを特定したら、それらがGCされないようにしているものを見つけようとします。おそらく、ログ記録などのために、忘れられたリストまたは未使用のリストに詰め込まれているのでしょう。


編集:現時点ではメモリ領域と特定の配列サイズを無視します。リークを引き起こすこれらの文字列で何が行われているのかを把握するだけです。プログラムがこれらの文字列を作成または操作したときにトレースするオブジェクトが少ない場合、!GCRootを試してください。

1
HUAGHAGUAH

すばらしい質問です。質問を読んで学びました。

逆シリアル化コードパスの他の部分も大きなオブジェクトヒープを使用しているため、断片化が発生していると思います。すべての文字列が同じ時間にインターンされた場合、あなたは大丈夫だと思います。

.netガベージコレクターがどれだけ優れているかを考えれば、逆シリアル化コードパスに通常の文字列オブジェクトを作成させるだけで十分でしょう。必要性が証明されるまで、より複雑なことはしないでください。

私はせいぜい、あなたが見た最後のいくつかの文字列のハッシュテーブルを保持し、これらを再利用することを見るでしょう。ハッシュテーブルのサイズを制限し、テーブルの作成時にサイズを渡すことで、ほとんどの断片化を停止できます。次に、最近見たことがない文字列をハッシュテーブルから削除して、サイズを制限する方法が必要です。 しかし、デシリアライゼーションコードパスが作成する文字列が短命である場合は、とにかくあまり得られません

1
Ian Ringrose
1
Jose Gzz