[c#] 대형 개체 힙 조각화

내가 작업중인 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 조각화로 인해 응용 프로그램이 누출됩니다.

우리 애플리케이션의 경우 역 직렬화 코드 경로에 비 정렬 화 중에 엔티티 식별자를 인턴하는 일반 코드가 있습니다. 이제 이것이 범인이라고 강력하게 의심합니다. 그러나 개발자의 의도는 동일한 엔터티가 여러 번 역 직렬화되는 경우 식별자 문자열의 한 인스턴스 만 메모리에 유지되도록하고 싶었 기 때문에 분명히 좋았습니다.



답변

CLR은 LOH를 사용하여 몇 가지 개체 (예 : 인턴 문자열에 사용되는 배열) 를 미리 할당합니다 . 이들 중 일부는 85000 바이트 미만이므로 일반적으로 LOH에 할당되지 않습니다.

구현 세부 사항이지만 그 이유는 프로세스 자체가 지속되는 한 살아남 아야하는 인스턴스의 불필요한 가비지 수집을 피하기위한 것입니다.

또한 다소 난해한 최적화로 인해 double[]1000 개 이상의 요소도 LOH에 할당됩니다.


답변

.NET Framework 4.5.1에는 가비지 수집 중에 LOH (대형 개체 힙)를 명시 적으로 압축하는 기능이 있습니다.

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

GCSettings.LargeObjectHeapCompactionMode 에서 자세한 정보를 참조하십시오.


답변

GC가 작동하는 방식에 대한 설명과 수명이 긴 개체가 2 세대로 끝나고 LOH 개체의 수집이 전체 수집에서만 발생하는 부분에 대한 설명을 읽을 때 2 세대 수집과 마찬가지로 마음에 떠오르는 아이디어가 있습니다. .. 2 세대와 대형 개체가 함께 수집 될 것이므로 동일한 힙에 보관하지 않는 이유는 무엇입니까?

그것이 실제로 일어난다면 작은 물체가 LOH와 같은 장소에서 어떻게 끝나는 지 설명 할 것입니다.

그래서 당신의 문제는 나에게 발생하는 아이디어에 대한 꽤 좋은 반박으로 보일 것입니다. 그것은 LOH의 단편화를 초래할 것입니다.

요약 : LOH와 2 세대가 동일한 힙 영역을 공유 하여 문제를 설명 수 있지만 이것이 이것이 설명이라는 증거는 아닙니다.

업데이트 : 결과물 !dumpheap -stat이이 이론을 물 밖으로 날려 버렸습니다! 2 세대와 LOH에는 자체 지역이 있습니다.


답변

형식을 애플리케이션으로 인식 할 수있는 경우이 문자열 형식을 생성하는 코드를 식별하지 않은 이유는 무엇입니까? 여러 가능성이있는 경우 고유 한 데이터를 추가하여 어떤 코드 경로가 범인인지 알아 내십시오.

배열이 큰 해제 된 항목과 인터리브된다는 사실은 원래 쌍을 이루었거나 적어도 관련이 있다고 추측하게합니다. 해제 된 개체를 식별하여 개체 및 관련 문자열을 생성하는 항목을 파악하십시오.

이러한 문자열을 생성하는 항목을 식별 한 후에는 GC가되지 않도록하는 것이 무엇인지 파악하십시오. 아마도 그들은 로깅 목적이나 유사한 목적으로 잊혀지거나 사용되지 않은 목록에 채워지고있을 것입니다.


편집 : 잠시 동안 메모리 영역과 특정 배열 크기를 무시하십시오.이 문자열로 누수를 일으키는 원인이 무엇인지 파악하십시오. 추적 할 개체가 적을 때 프로그램이 이러한 문자열을 한두 번만 만들거나 조작 한 경우! GCRoot를 사용해보십시오.


답변

좋은 질문입니다. 질문을 읽고 배웠습니다.

나는 deserialization 코드 경로의 다른 비트도 큰 개체 힙을 사용하므로 조각화가 발생한다고 생각합니다. 모든 현이 같은 시간에 인턴 되었다면 괜찮을 것 같아요.

.net 가비지 수집기가 얼마나 좋은지 감안할 때 deserialization 코드 경로가 일반 문자열 개체를 생성하도록하는 것만으로도 충분할 수 있습니다. 필요성이 입증 될 때까지 더 복잡한 작업을하지 마십시오.

나는 당신이 본 마지막 몇 가지 문자열의 해시 테이블을 유지하고 재사용하는 것을 기껏해야합니다. 해시 테이블 크기를 제한하고 테이블을 만들 때 크기를 전달하면 대부분의 조각화를 중지 할 수 있습니다. 그런 다음 해시 테이블에서 최근에 보지 않은 문자열을 제거하여 크기를 제한하는 방법이 필요합니다. 그러나 역 직렬화 코드 경로가 생성하는 문자열이 어차피 수명이 짧다면 아무리 많이 얻지 못할 것입니다.


답변

여기에 정확한 식별 할 수있는 방법의 커플 호출 스택LOH의 할당을.

LOH 조각화를 방지하려면 많은 개체를 미리 할당하고 고정합니다. 필요할 때 이러한 개체를 재사용하십시오. 다음은 LOH Fragmentation에 대한 게시물 입니다. 이와 같은 것이 LOH 단편화를 방지하는 데 도움이 될 수 있습니다.


답변