대형 개체 힙 조각화


97

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

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


2
좋은 질문입니다. 제 응용 프로그램에서 동일한 점을 발견했습니다. 큰 블록을 청소 한 후 LOH에 작은 물체가 남아있어 조각화 문제가 발생합니다.
Reed Copsey

2
동의합니다. 좋은 질문입니다. 답을 주시하겠습니다.
Charlie Flowers

2
매우 흥미로운. 디버그하는 것이 상당히 문제인 것 같습니다!
Matt Jordan

답변:


47

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

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

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


문제가있는 개체는 앱 코드에 의해 생성되는 것으로 알고있는 문자열에 대한 참조를 포함하는 object []입니다. 이는 앱이 object []를 생성 중이거나 (이 증거를 볼 수 없음) CLR의 일부 (예 : 직렬화)가이를 사용하여 응용 프로그램 개체에 대해 작업하고 있음을 의미합니다.
Paul Ruane

1
인턴 문자열에 사용되는 내부 구조 일 수 있습니다. 자세한 내용은이 질문에 대한 내 대답을 확인하십시오. stackoverflow.com/questions/372547/…
Brian Rasmussen

아, 매우 흥미로운 단서입니다. 감사합니다. 인턴 테이블을 완전히 잊었습니다. 나는 우리 개발자 중 한 명이 예리한 내부자라는 것을 알고 있으므로 이것은 확실히 조사 할 것입니다.
Paul Ruane

1
85000 바이트 또는 84 * 1024 = 87040 바이트?
Peter Mortensen 2013 년

5
85000 바이트. 85000-12의 바이트 배열 (길이 크기, MT, 동기화 블록)을 만들고 GC.GetGeneration인스턴스를 호출 하여이를 확인할 수 있습니다. 그러면 Gen2가 반환됩니다. API는 Gen2와 LOH를 구분하지 않습니다. 배열을 1 바이트 더 작게 만들면 API가 Gen0을 반환합니다.
Brian Rasmussen 2013 년


2

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

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

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

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

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


! eeheap을 사용하여 각 힙을 구성하는 세그먼트를 표시하십시오. Gen 0과 Gen 1은 하나의 세그먼트 (동일한 세그먼트)에 있으며, Gen 2와 LOH는 모두 여러 세그먼트를 할당 할 수 있지만 각 힙에 대한 세그먼트는 별도로 유지됩니다.
Paul Ruane

네, 봤어요, 감사합니다. 이 동작을 훨씬 더 명확하게 보여주기 때문에! eeheaps 명령을 언급하고 싶었습니다.
Paul Ruane

주 GC의 효율성은 대부분 개체를 재배치 할 수 있으므로 주 힙에 적은 수의 사용 가능한 메모리 영역 만 있다는 사실에서 비롯됩니다. 수집하는 동안 기본 힙의 개체가 고정 된 경우 고정 된 개체의 위와 아래 공간을 별도로 추적해야 할 수 있지만 고정 된 개체의 수가 일반적으로 매우 적기 때문에 GC가해야하는 별도의 영역의 수가됩니다. 과정. 재배치 가능 및 재배치 불가능 (대형) 개체를 동일한 힙에 혼합하면 성능이 저하됩니다.
supercat 2015 년

더 흥미로운 질문은 .NET이 doubleLOH에 1000 개보다 큰 배열을 배치하는 이유입니다 . GC를 조정하여 8 바이트 경계에 정렬되도록하는 것이 아닙니다. 실제로 32 비트 시스템에서도 캐시 동작으로 인해 할당 된 크기가 8 바이트의 배수 인 모든 개체에 8 바이트 정렬을 적용하면 성능이 향상 될 수 있습니다. 그렇지 않으면 double[]캐시에 맞춰 많이 사용되는 성능이 그렇지 않은 것보다 더 좋을 수 있지만 크기가 사용량과 관련되는 이유를 모르겠습니다.
supercat

@supercat 또한 두 힙은 할당에서도 매우 다르게 작동합니다. 기본 힙은 (현재) 기본적으로 할당 패턴의 스택입니다. 항상 맨 위에 할당하고 여유 공간을 무시합니다. 압축이 오면 여유 공간이 압착됩니다. 이렇게하면 할당이 거의 작동하지 않으며 데이터 지역성을 돕습니다. 반면에 LOH에 할당하는 것은 malloc이 작동하는 방식과 유사합니다. 즉, 할당중인 것을 보관할 수있는 첫 번째 무료 스팟을 찾아 거기에 할당합니다. 큰 개체에 대한 것이기 때문에 데이터 지역성이 주어지고 할당에 대한 패널티도 나쁘지 않습니다.
Luaan

1

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

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

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


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


문자열은 Guid (우리가 사용하는)와 쉽게 식별 할 수있는 문자열 키의 혼합입니다. 생성 된 위치를 볼 수 있지만 객체 배열에 (직접) 추가되지 않으며 명시 적으로 128 개의 요소 배열을 생성하지 않습니다. 이 작은 배열은 시작하려면 LOH에 있지 않아야합니다.
Paul Ruane

1

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

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

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

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


1

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

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


여기에 고정하는 것이 도움이되는 이유를 알 수 없습니다. LOH의 BTW 대형 오브젝트는 어쨌든 GC에 의해 이동되지 않습니다. 그래도 구현 세부 사항입니다.
user492238 2011

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.