Fragmentacja sterty dużych obiektów

97

Aplikacja C # / .NET, nad którą pracuję, cierpi na powolny wyciek pamięci. Użyłem CDB z SOS, aby spróbować określić, co się dzieje, ale dane nie wydają się mieć żadnego sensu, więc miałem nadzieję, że któryś z was mógł już tego doświadczyć.

Aplikacja działa na platformie 64-bitowej. Ciągle oblicza i serializuje dane do zdalnego hosta i dość mocno uderza w stertę dużych obiektów (LOH). Jednak spodziewam się, że większość obiektów LOH będzie przejściowych: po zakończeniu obliczeń i wysłaniu ich do zdalnego hosta pamięć powinna zostać zwolniona. Widzę jednak dużą liczbę (na żywo) tablic obiektów przeplatanych wolnymi blokami pamięci, np. Pobierając losowy segment z 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
...

Oczywiście spodziewałbym się, że tak będzie w przypadku, gdy moja aplikacja podczas każdego obliczenia tworzy długowieczne, duże obiekty. (Robi to i zgadzam się, że będzie pewien stopień fragmentacji LOH, ale nie stanowi to problemu). Problemem są bardzo małe (1056 bajtów) tablice obiektów, które można zobaczyć na powyższym zrzucie, których nie widzę w kodzie są tworzone i które w jakiś sposób pozostają zakorzenione.

Należy również zauważyć, że CDB nie zgłasza typu, gdy segment sterty jest zrzucany: nie jestem pewien, czy jest to powiązane, czy nie. Jeśli zrzucę zaznaczony (<-) obiekt, CDB / SOS zgłasza to dobrze:

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

Wszystkie elementy tablicy obiektów to ciągi znaków, które są rozpoznawalne jako z naszego kodu aplikacji.

Ponadto nie mogę znaleźć ich korzeni GC, ponieważ polecenie! GCRoot zawiesza się i nigdy nie wraca (próbowałem nawet zostawić je na noc).

Więc byłbym bardzo wdzięczny, gdyby ktoś mógł rzucić trochę światła na to, dlaczego te małe (<85k) tablice obiektów trafiają do LOH: w jakich sytuacjach .NET umieści tam małą tablicę obiektów? Czy zdarza się, że ktoś zna alternatywny sposób ustalania korzeni tych obiektów?


Zaktualizuj 1

Inną teorią, na którą wpadłem późno wczoraj, jest to, że te tablice obiektów na początku były duże, ale zostały zmniejszone, pozostawiając bloki wolnej pamięci, które są widoczne w zrzutach pamięci. To, co mnie podejrzewa, to fakt, że tablice obiektów zawsze wydają się mieć długość 1056 bajtów (128 elementów), 128 * 8 dla odniesień i 32 bajty narzutu.

Chodzi o to, że być może jakiś niebezpieczny kod w bibliotece lub w środowisku CLR psuje liczbę elementów pola w nagłówku tablicy. Wiem, że to długa szansa ...


Zaktualizuj 2

Dzięki Brianowi Rasmussenowi (patrz zaakceptowana odpowiedź) problem został zidentyfikowany jako fragmentacja LOH spowodowana tabelą strun! Napisałem szybką aplikację testową, aby to potwierdzić:

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();
}

Aplikacja najpierw tworzy i wyłuskuje unikalne ciągi w pętli. Ma to na celu tylko udowodnienie, że w tym scenariuszu pamięć nie przecieka. Oczywiście nie powinno i tak nie jest.

W drugiej pętli tworzone i internalizowane są unikalne ciągi. Ta akcja zakorzenia ich w tabeli stażystów. Nie zdawałem sobie sprawy, jak reprezentowana jest tabela stażystów. Wygląda na to, że składa się z zestawu stron - tablic obiektów złożonych ze 128 elementów łańcuchowych - które są tworzone w LOH. Jest to bardziej widoczne w 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)

Wykonanie zrzutu segmentu LOH ujawnia wzór, który widziałem w nieszczelnej aplikacji:

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

Zauważ, że rozmiar tablicy obiektów to 528 (zamiast 1056), ponieważ moja stacja robocza jest 32-bitowa, a serwer aplikacji jest 64-bitowy. Tablice obiektów mają nadal 128 elementów.

Zatem morał w tej historii polega na bardzo ostrożnym internowaniu. Jeśli nie wiadomo, czy łańcuch, który internujesz, należy do skończonego zestawu, wówczas aplikacja będzie przeciekać z powodu fragmentacji LOH, przynajmniej w wersji 2 CLR.

W przypadku naszej aplikacji w ścieżce kodu deserializacji znajduje się ogólny kod, który internuje identyfikatory jednostek podczas usuwania błędów: teraz mocno podejrzewam, że to jest winowajca. Jednak intencje programisty były oczywiście dobre, ponieważ chcieli się upewnić, że jeśli ta sama jednostka jest deserializowana wiele razy, tylko jedno wystąpienie ciągu identyfikatora zostanie zachowane w pamięci.

Paul Ruane
źródło
2
Świetne pytanie - zauważyłem to samo w mojej aplikacji. Małe przedmioty pozostawione w LOH po oczyszczeniu dużych bloków powodują problemy z fragmentacją.
Reed Copsey,
2
Zgadzam się, świetne pytanie. Będę szukał odpowiedzi.
Charlie Flowers
2
Bardzo interesujące. Wygląda na to, że debugowanie było sporym problemem!
Matt Jordan

Odpowiedzi:

47

Środowisko CLR używa LOH do wstępnego przydzielenia kilku obiektów (takich jak tablica używana dla wewnętrznych ciągów ). Niektóre z nich mają mniej niż 85000 bajtów i dlatego normalnie nie są przydzielane na LOH.

Jest to szczegół implementacji, ale zakładam, że powodem tego jest uniknięcie niepotrzebnego zbierania śmieci instancji, które mają przetrwać tak długo, jak sam proces.

Również z powodu nieco ezoterycznej optymalizacji każdy double[]z 1000 lub więcej elementów jest również przydzielany na LOH.

Brian Rasmussen
źródło
Problematyczne obiekty to obiekty [] zawierające odwołania do ciągów znaków, o których wiem, że są tworzone przez kod aplikacji. Oznacza to, że aplikacja tworzy obiekty [] s (nie widzę na to dowodów) lub że jakaś część środowiska CLR (na przykład serializacja) używa ich do pracy z obiektami aplikacji.
Paul Ruane,
1
Może to być struktura wewnętrzna używana dla strun internowanych. Proszę sprawdzić moją odpowiedź na to pytanie, aby uzyskać więcej informacji: stackoverflow.com/questions/372547/ ...
Brian Rasmussen
Ach, to bardzo interesujący trop, dzięki. Zupełnie zapomniałem o stole stażystów. Wiem, że jeden z naszych programistów jest zapalonym stażystą, więc jest to zdecydowanie coś, co zbadam.
Paul Ruane
1
85000 bajtów lub 84 * 1024 = 87040 bajtów?
Peter Mortensen
5
85000 bajtów. Możesz to sprawdzić, tworząc tablicę bajtów 85000-12 (rozmiar długości, MT, blok synchronizacji) i wywołując GC.GetGenerationinstancję. To zwróci Gen2 - interfejs API nie rozróżnia Gen2 i LOH. Zmniejsz tablicę o jeden bajt, a interfejs API zwróci Gen0.
Brian Rasmussen
13

NET Framework 4.5.1 umożliwia jawne kompaktowanie sterty dużych obiektów (LOH) podczas wyrzucania elementów bezużytecznych.

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

Zobacz więcej informacji w GCSettings.LargeObjectHeapCompactionMode

Andre Abrantes
źródło
2

Czytając opisy działania GC i części o tym, jak długowieczne obiekty trafiają do generacji 2, a zbieranie obiektów LOH odbywa się tylko przy pełnej kolekcji - podobnie jak kolekcja generacji 2, przychodzi mi na myśl idea. .. dlaczego nie po prostu trzymać generacji 2 i dużych obiektów w tej samej stercie, ponieważ zostaną zebrane razem?

Jeśli tak się faktycznie dzieje, wyjaśniałoby to, w jaki sposób małe obiekty trafiają do tego samego miejsca co LOH - jeśli żyją wystarczająco długo, aby trafić do generacji 2.

Tak więc twój problem wydawałby się całkiem niezłym obaleniem pomysłu, który mi przychodzi do głowy - spowodowałby fragmentację LOH.

Podsumowanie: twój problem można wyjaśnić tym, że LOH i pokolenie 2 mają ten sam region sterty, chociaż w żadnym wypadku nie jest to dowód na to, że jest to wyjaśnienie.

Aktualizacja: wynik !dumpheap -statprawie wyrzuca tę teorię z wody! Pokolenie 2 i LOH mają własne regiony.

Daniel Earwicker
źródło
Użyj! Eeheap, aby wyświetlić segmenty, które składają się na każdy stos. Gen 0 i gen 1 żyją w jednym segmencie (ten sam segment), gen 2 i LOH mogą przydzielać wiele segmentów, ale segmenty dla każdego sterty pozostają oddzielne.
Paul Ruane
Tak, widziałem, dzięki. Chciałem tylko wspomnieć o poleceniu! Eeheaps, ponieważ pokazuje to zachowanie w znacznie wyraźniejszy sposób.
Paul Ruane,
Wydajność głównego GC wynika w dużej mierze z faktu, że może on przemieszczać obiekty, więc na stercie głównym będzie tylko niewielka liczba wolnych obszarów pamięci. Jeśli obiekt na głównym stercie jest przypięty podczas zbierania, przestrzeń powyżej i poniżej przypiętego obiektu może wymagać osobnego śledzenia, ale ponieważ liczba przypiętych obiektów jest zwykle bardzo mała, podobnie będzie z liczbą oddzielnych obszarów, które GC musi tor. Mieszanie obiektów relokowalnych i nieprzenoszalnych (dużych) w tej samej stercie zmniejszyłoby wydajność.
supercat
Bardziej interesującym pytaniem jest, dlaczego .NET umieszcza doubletablice większe niż 1000 elementów w LOH, zamiast modyfikować GC, aby zapewnić, że są wyrównane do granic 8-bajtowych. Właściwie nawet w systemie 32-bitowym spodziewałbym się, że ze względu na zachowanie pamięci podręcznej, narzucenie 8-bajtowego wyrównania na wszystkie obiekty, których przydzielony rozmiar jest wielokrotnością 8 bajtów, prawdopodobnie byłoby wygraną w zakresie wydajności. W przeciwnym razie, chociaż wydajność często używanego double[], wyrównanego do pamięci podręcznej, byłaby lepsza niż takiej, która nie jest, nie wiem, dlaczego rozmiar miałby korelować z użyciem.
supercat
@supercat Również te dwie sterty zachowują się bardzo różnie w alokacji. Główny stert jest (w tej chwili) w zasadzie stosem we wzorcach alokacji - zawsze przydziela na górze, ignorując wszelkie wolne miejsce - gdy przychodzi zagęszczenie, wolne miejsca są wyciskane. To sprawia, że ​​alokacja jest prawie niemożliwa i pomaga w lokalizacji danych. Z drugiej strony, alokacja na LOH jest podobna do tego, jak działa malloc - znajdzie pierwsze wolne miejsce, które może pomieścić to, co przydzielasz, i tam je przydzieli. Ponieważ dotyczy to dużych obiektów, lokalność danych jest pewna, a kara za alokację nie jest taka zła.
Luaan
1

Jeśli format jest rozpoznawalny jako Twoja aplikacja, dlaczego nie zidentyfikowałeś kodu, który generuje ten format ciągu? Jeśli istnieje kilka możliwości, spróbuj dodać unikalne dane, aby dowiedzieć się, która ścieżka kodu jest przyczyną.

Fakt, że tablice są przeplatane dużymi uwolnionymi elementami, prowadzi mnie do przypuszczenia, że ​​pierwotnie były one sparowane lub przynajmniej powiązane. Spróbuj zidentyfikować zwolnione obiekty, aby dowiedzieć się, co je generowało i powiązane ciągi.

Kiedy już zidentyfikujesz, co generuje te ciągi, spróbuj dowiedzieć się, co powstrzymałoby je przed otrzymaniem GC. Być może są umieszczane na zapomnianej lub nieużywanej liście do celów logowania lub czegoś podobnego.


EDYCJA: Zignoruj ​​na razie region pamięci i określony rozmiar tablicy: po prostu dowiedz się, co jest robione z tymi ciągami, aby spowodować wyciek. Wypróbuj! GCRoot, gdy program utworzył lub przetworzył te ciągi tylko raz lub dwa razy, gdy jest mniej obiektów do śledzenia.

HUAGHAGUAH
źródło
Łańcuchy są połączeniem Guidów (których używamy) i kluczy ciągów, które są łatwe do zidentyfikowania. Widzę, gdzie są generowane, ale nigdy nie są (bezpośrednio) dodawane do tablic obiektów i nie tworzymy jawnie tablic 128-elementowych. Te małe tablice nie powinny jednak znajdować się w LOH na początku.
Paul Ruane
1

Świetne pytanie, nauczyłem się czytając pytania.

Myślę, że inne fragmenty ścieżki kodu deserializacji również używają dużej sterty obiektów, stąd fragmentacja. Gdyby wszystkie struny były internowane w TYM SAMYM czasie, myślę, że wszystko byłoby w porządku.

Biorąc pod uwagę, jak dobry jest odśmiecacz .net, wystarczy pozwolić ścieżce kodu deserializacji na utworzenie normalnego obiektu łańcuchowego. Nie rób nic bardziej złożonego, dopóki potrzeba nie zostanie udowodniona.

Chciałbym co najwyżej spojrzeć na zachowanie tabeli skrótów ostatnich kilku łańcuchów, które widzieliście, i ponowne ich użycie. Ograniczając rozmiar tabeli skrótów i przekazując rozmiar podczas tworzenia tabeli, możesz zatrzymać większość fragmentacji. Następnie potrzebujesz sposobu na usunięcie ciągów, których ostatnio nie widziałeś, z tablicy skrótów, aby ograniczyć ich rozmiar. Ale jeśli łańcuchy utworzone przez ścieżkę kodu deserializacji są krótkotrwałe i tak niewiele zyskasz, jeśli w ogóle.

Ian Ringrose
źródło
1

Oto kilka sposobów, aby dokładnie określić call-stos z LOH alokacji.

Aby uniknąć fragmentacji LOH, przydziel wstępnie dużą tablicę obiektów i przypnij je. W razie potrzeby użyj tych obiektów ponownie. Oto post na temat fragmentacji LOH. Coś takiego mogłoby pomóc w uniknięciu fragmentacji LOH.

Naveen
źródło
Nie rozumiem, dlaczego przypinanie tutaj powinno pomóc? BTW i tak duże obiekty na LOH nie są przenoszone przez GC. Jest to jednak szczegół implementacji.
user492238