Programming

대형 개체 힙 조각화

procodes 2020. 8. 25. 20:31
반응형

대형 개체 힙 조각화


내가 작업중인 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

Note that the object array size is 528 (rather than 1056) because my workstation is 32 bit and the application server is 64 bit. The object arrays are still 128 elements long.

So the moral to this story is to be very careful interning. If the string you are interning is not known to be a member of a finite set then your application will leak due to fragmentation of the LOH, at least in version 2 of the CLR.

In our application's case, there is general code in the deserialisation code path that interns entity identifiers during unmarshalling: I now strongly suspect this is the culprit. However, the developer's intentions were obviously good as they wanted to make sure that if the same entity is deserialised multiple times then only one instance of the identifier string will be maintained in memory.


The CLR uses the LOH to preallocate a few objects (such as the array used for interned strings). Some of these are less than 85000 bytes and thus would not normally be allocated on the LOH.

It is an implementation detail, but I assume the reason for this is to avoid unnecessary garbage collection of instances that are supposed to survive as long as the process it self.

Also due to a somewhat esoteric optimization, any double[] of 1000 or more elements is also allocated on the LOH.


The .NET Framework 4.5.1, has the ability to explicitly compact the large object heap (LOH) during garbage collection.

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

See more info in GCSettings.LargeObjectHeapCompactionMode


When reading descriptions of how GC works, and the part about how long-lived objects end up in generation 2, and the collection of LOH objects happens at full collection only - as does collection of generation 2, the idea that springs to mind is... why not just keep generation 2 and large objects in the same heap, as they're going to get collected together?

If that's what actually happens then it would explain how small objects end up in the same place as the LOH - if they're long lived enough to end up in generation 2.

And so your problem would appear to be a pretty good rebuttal to the idea that occurs to me - it would result in the fragmentation of the LOH.

Summary: your problem could be explained by the LOH and generation 2 sharing the same heap region, although that is by no means proof that this is the explanation.

Update: the output of !dumpheap -stat pretty much blows this theory out of the water! The generation 2 and LOH have their own regions.


If the format is recognizable as your application, why haven't you identified the code that is generating this string format? If there's several possibilities, try adding unique data to figure out which code path is the culprit.

The fact that the arrays are interleaved with large freed items leads me to guess that they were originally paired or at least related. Try to identify the freed objects to figure out what was generating them and the associated strings.

Once you identify what is generating these strings, try to figure out what would be keeping them from being GCed. Perhaps they're being stuffed in a forgotten or unused list for logging purposes or something similar.


EDIT: Ignore the memory region and the specific array size for the moment: just figure out what is being done with these strings to cause a leak. Try the !GCRoot when your program has created or manipulated these strings just once or twice, when there's fewer objects to trace.


Great question, I learned by reading the questions.

I think other bit of the deserialisation code path are also using the large object heap, hence the fragmentation. If all the strings were interned at the SAME time, I think you would be ok.

Given how good the .net garbage collector is, just letting the deserialisation code path create normal string object is likely to be good enough. Don't do anything more complex until the need is proven.

I would at most look at keeping a hash table of the last few strings you have seen and reusing these. By limiting the hash table size and passing the size in when you create the table you can stop most fragmentation. You then need a way to remove strings you have not seen recently from the hash table to limit it’s size. But if the strings the deserialisation code path create are short lived anyway you will not gain much if anything.


Here are couple of ways to Identify the exact call-stack of LOH allocation.

And to avoid LOH fragmentation Pre-allocate large array of objects and pin them. Reuse these objects when needed. Here is post on LOH Fragmentation. Something like this could help in avoiding LOH fragmentation.


According to http://webcache.googleusercontent.com/search?q=cache:l3Ppy_eFoAAJ:www.wintellect.com/devcenter/sloscialo/hey-who-stole-all-my-memory+&cd=5&hl=en&ct=clnk&gl=mx&client=firefox-b

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

참고URL : https://stackoverflow.com/questions/686950/large-object-heap-fragmentation

반응형