私が取り組んでいるC#/。NETアプリケーションは、遅いメモリリークに悩まされています。私はSOSでCDBを使用して何が起こっているのかを判断しようとしましたが、データが意味をなさないように思われるので、あなたの1人が以前にこれを経験したことがあると期待していました。
アプリケーションは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つのインスタンスのみがメモリに保持されるようにしたかったので、開発者の意図は明らかに良かったです。