ラージオブジェクトヒープの断片化


97

私が取り組んでいる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

Brian Rasmussenのおかげで(受け入れられた回答を参照)、問題は文字列のインターンテーブルによって引き起こされた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要素の長さです。

したがって、この話の教訓は、非常に注意深くインターンすることです。あなたがインターンしている文字列が有限集合のメンバーであることが知られていない場合、少なくともCLRのバージョン2では、LOHの断片化のためにアプリケーションがリークします。

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


2
すばらしい質問です。アプリケーションで同じことに気づきました。大きなブロックが削除された後、LOHに小さなオブジェクトが残っており、断片化の問題が発生しています。
リードコプシー、

2
私は同意します、素晴らしい質問です。答えを待ちます。
チャーリーフラワー

2
とても興味深い。デバッグするのはかなり問題だったようですね!
マットジョーダン

回答:


46

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

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

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


問題のあるオブジェクトは、アプリのコードによって作成されていることがわかっている文字列への参照を含むobject []です。これは、アプリがオブジェクト[]を作成していること(これの証拠を確認できない)、またはCLRの一部(シリアル化など)がそれらを使用してアプリケーションオブジェクトを処理していることを意味します。
ポールルアン

1
それは、抑留された文字列に使用される内部構造である可能性があります。詳細については、この質問のために私の答えを確認してください:stackoverflow.com/questions/372547/...
ブライアンラスムッセン

ああ、これは非常に興味深いリードです、ありがとう。インターンテーブルをすっかり忘れていました。私の開発者の1人は鋭敏なインターナーであることを知っているので、これは間違いなく私が調査するものです。
ポールルアン

1
85000バイトまたは84 * 1024 = 87040バイト?
Peter Mortensen 2013年

5
85000バイト。これを確認するには、85000-12(長さのサイズ、MT、同期ブロック)のバイト配列を作成GC.GetGenerationし、インスタンスを呼び出します。これはGen2を返します-APIはGen2とLOHを区別しません。配列を1バイト小さくすると、APIはGen0を返します。
Brian Rasmussen 2013年


2

GCがどのように動作するかの説明、および寿命の長いオブジェクトが第2世代にどのように到達するかに関する部分を読むとき、LOHオブジェクトのコレクションは、完全なコレクションでのみ発生します。 ..第2世代と大きなオブジェクトを一緒に収集するので、それらを同じヒープに保持しないのはなぜですか?

それが実際に起こっていることである場合、小さなオブジェクトがLOHと同じ場所にどのように到達するかを説明します。

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

概要:LOHとジェネレーション2が同じヒープ領域を共有することで問題説明される可能性がありますが、これが説明であるという証拠にはなりません。

更新:の出力は、!dumpheap -statこの理論を水から吹き飛ばします!第2世代とLOHには独自のリージョンがあります。


!eeheapを使用して、各ヒープを構成するセグメントを表示します。Gen 0とGen 1は1つのセグメント(同じセグメント)にあり、gen 2とLOHは両方とも複数のセグメントを割り当てることができますが、各ヒープのセグメントは分離したままです。
ポールルアン

はい、見ました、ありがとう。!eeheapsコマンドは、この動作をより明確に示すので、言及したかっただけです。
ポールルアン

メインGCの効率の大部分は、オブジェクトを再配置できるため、メインヒープにメモリの空き領域が少数しか存在しないという事実に由来します。コレクション中にメインヒープ上のオブジェクトが固定されている場合、固定されたオブジェクトの上下のスペースを個別に追跡する必要がある場合がありますが、固定されたオブジェクトの数は通常非常に少ないため、GCが必要とする個別の領域の数も同様です。追跡。同じヒープ内で再配置可能オブジェクトと再配置不可能な(ラージ)オブジェクトを混在させると、パフォーマンスが低下します。
スーパーキャット2015年

より興味深い質問は、.NETがdouble、GCを微調整するのではなく、1000要素を超える配列をLOHに配置して、8バイト境界に整列するようにする理由です。実際、32ビットシステムであっても、キャッシュの動作により、割り当てられたサイズが8バイトの倍数であるすべてのオブジェクトに8バイトのアラインメントを課すと、おそらくパフォーマンスが向上すると思います。それ以外の場合、double[]使用頻度の高いキャッシュアラインメントのパフォーマンスはそうでないパフォーマンスよりも優れていますが、サイズが使用量と相関する理由がわかりません。
スーパーキャット2015年

@supercatまた、2つのヒープの割り当ての動作も非常に異なります。メインヒープは(現時点では)基本的に割り当てパターンのスタックです-空き領域を無視して常に最上部に割り当てられます-圧縮が行われると、空き領域が絞り出されます。これにより、割り当てはほとんど機能せず、データの局所性が向上します。一方、LOHでの割り当ては、mallocの動作に似ています。割り当てているものを保持できる最初の空きスポットを見つけ、そこに割り当てます。ラージオブジェクト用であるため、データの局所性は一定であり、割り当てのペナルティはそれほど悪くありません。
Luaan

1

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

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

これらの文字列を生成しているものを特定したら、それらがGCされないようにするために何が起こっているのかを理解してください。おそらくそれらは、ロギング目的または類似の目的で、忘れられたリストまたは未使用のリストに詰め込まれています。


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


文字列は、Guid(私たちが使用します)と文字列キーを組み合わせたもので、簡単に識別できます。生成された場所を確認できますが、オブジェクト配列に(直接)追加されることはなく、128要素の配列を明示的に作成しません。ただし、これらの小さなアレイは、最初からLOHに含めないでください。
ポールルアン

1

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

逆シリアル化コードパスの他のビットも大きなオブジェクトヒープを使用しているため、断片化が発生していると思います。同じ時にすべての文字列が抑留されていれば、大丈夫だと思います。

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

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


1

ここでは、正確な同定するための方法はいくつかあり、コールスタックLOHの割り当てを。

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


なぜここにピン留めすることが役立つのかわかりませんか?ともかく、LOH上の大きなオブジェクトは、GCによって移動されません。ただし、その実装の詳細。
user492238 2011

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.