내가 작업중인 C # /. NET 응용 프로그램은 느린 메모리 누수로 고통 받고 있습니다. 나는 무슨 일이 일어나고 있는지 확인하기 위해 SOS와 함께 CDB를 사용했지만 데이터가 이해가되지 않는 것 같아서 여러분 중 한 명이 전에 이것을 경험했을 수 있기를 바랐습니다.
애플리케이션이 64 비트 프레임 워크에서 실행 중입니다. 지속적으로 데이터를 계산하고 원격 호스트에 직렬화하고 있으며 LOH (Large Object Heap)에 상당한 영향을 미치고 있습니다. 그러나 내가 예상하는 대부분의 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
어제 늦게 생각해 낸 또 다른 이론은 이러한 객체 배열이 크게 시작되었지만 축소되어 메모리 덤프에 분명한 여유 메모리 블록이 남아 있다는 것입니다. 나를 의심스럽게 만드는 것은 객체 배열이 항상 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();
}
응용 프로그램은 먼저 루프에서 고유 한 문자열을 만들고 역 참조합니다. 이것은이 시나리오에서 메모리가 누출되지 않는다는 것을 증명하기위한 것입니다. 당연히 그렇게해서는 안되며 그렇지 않습니다.
두 번째 루프에서는 고유 한 문자열이 생성되고 인턴됩니다. 이 작업은 인턴 테이블에 뿌리를 둡니다. 내가 깨닫지 못한 것은 인턴 테이블이 어떻게 표현되는지입니다. 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 조각화로 인해 응용 프로그램이 누출됩니다.
우리 애플리케이션의 경우 역 직렬화 코드 경로에 비 정렬 화 중에 엔티티 식별자를 인턴하는 일반 코드가 있습니다. 이제 이것이 범인이라고 강력하게 의심합니다. 그러나 개발자의 의도는 동일한 엔터티가 여러 번 역 직렬화되는 경우 식별자 문자열의 한 인스턴스 만 메모리에 유지되도록하고 싶었 기 때문에 분명히 좋았습니다.