私が取り組んでいる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つだけメモリに保持されるようにするため、開発者の意図は明らかに良かったです。