Halda velkých objektů v Windows systémech

Systém uvolňování paměti .NET rozděluje objekty na malé a velké objekty. Pokud je objekt velký, některé jeho atributy jsou významnější než v případě, že je objekt malý. Například komprimování to znamená, že jeho zkopírování do paměti jinde — na haldě může být — nákladné. Z tohoto důvodu systém uvolňování paměti umístí velké objekty na haldu velkých objektů (LOH). Tento článek popisuje, co kvalifikuje objekt jako velký objekt, jak velké objekty se shromažďují a jaký vliv mají velké objekty na výkon.

Důležité

Tento článek popisuje haldu velkých objektů v .NET Framework a .NET Core běžící pouze v Windows systémech. Nevztahuje se na LOH běžící na implementacích .NET na jiných platformách.

Jak objekt skončí na LOH

Pokud je objekt větší nebo roven 85 000 bajtů, považuje se za velký objekt. Toto číslo bylo určeno laděním výkonu. Pokud je žádost o přidělení objektu 85 000 nebo více bajtů, modul runtime ji přidělí k haldě velkého objektu.

Abyste pochopili, co to znamená, je užitečné prozkoumat některé základní informace o systému uvolňování paměti.

Systém uvolňování paměti je generationální kolektor. Má tři generace: generace 0, generace 1 a generace 2. Důvodem tří generací je to, že v dobře vyladěné aplikaci většina objektů v Gen0 zamíří. Například v serverové aplikaci by přidělení přidružená ke každému požadavku měla po dokončení požadavku vymknou. Žádosti o přidělení během letu se převedou do Gen1 a tam zamknou. Gen1 v podstatě funguje jako vyrovnávací paměť mezi malými objekty a dlouhodobými objekty.

Nově přidělené objekty tvoří novou generaci objektů a jsou implicitně generace 0 kolekcí. Pokud ale jde o velké objekty, jdou na haldu velkých objektů (LOH), která se někdy označuje jako generace 3. Generace 3 je fyzická generace, která se logicky shromažďuje jako součást generace 2.

Velké objekty patří do generace 2, protože se shromažďují pouze během kolekce generace 2. Když se shromažďuje generace, shromažďují se také všechny její mladší generace. Když například dojde ke globálním katalogem 1. generace, shromažďují se obě generace 1 i 0. A když dojde ke globálním katalogem 2. generace, shromažďová se celá halda. Z tohoto důvodu se uvolňování paměti 2. generace také nazývá úplný uvolňování paměti. Tento článek se týká uvolňování paměti generace 2 místo plného globálního katalogu, ale termíny jsou zaměnitelné.

Generace poskytují logické zobrazení haldy uvolňování paměti. Fyzicky se objekty nachází ve spravovaných segmentech haldy. Segment spravované haldy je blok paměti, který uvolňování paměti rezervuje z operačního systému voláním funkce VirtualAlloc jménem spravovaného kódu. Při načtení modulu CLR uvolňování paměti přidělí dva počáteční segmenty haldy: jeden pro malé objekty (malá halda objektu nebo SOH) a jeden pro velké objekty (halda velkého objektu).

Požadavky na přidělení jsou poté splněny umístěním spravovaných objektů do těchto spravovaných segmentů haldy. Pokud je objekt menší než 85 000 bajtů, je v segmentu pro SOH. V opačném případě se zasune do segmentu LOH. Segmenty jsou potvrzeny (v menších bloků), protože k nim je přiděleno více a více objektů. U SOH se objekty, které přežijí uvolňování paměti, povýší na další generaci. Objekty, které přežijí kolekci generace 0, se nyní považují za objekty generace 1 atd. Objekty, které přežijí nejstarší generaci, jsou však stále považovány za nejstarší generaci. Jinými slovy, přeživší z generace 2 jsou objekty generace 2. a přeživší z LOH jsou objekty LOH (které se shromažďují pomocí Gen2).

Uživatelský kód může přidělovat pouze ve generaci 0 (malé objekty) nebo LOH (velké objekty). Pouze uvolňování paměti může "přidělit" objekty v generaci 1 (povýšení přeživších z generace 0) a generace 2 (povýšení přeživších z generace 1).

Když se aktivuje uvolňování paměti, uvolňování paměti sleduje živé objekty a komprimuje je. Komprimace je ale nákladná, a proto uvolňování paměti otevře LOH. Vytvoří bezplatný seznam z neschocených objektů, které je možné později znovu použít ke splnění požadavků na přidělení velkých objektů. Sousední neschůdné objekty jsou tvořeny jedním volným objektem.

.NET Core a .NET Framework (počínaje .NET Framework 4.5.1) obsahují vlastnost, která uživatelům umožňuje určit, že se má LOH zkomprimovat při dalším úplném blokujícím uvolňování GCSettings.LargeObjectHeapCompactionMode paměti. A v budoucnu se .NET může rozhodnout, že LOH zkomprimuje automaticky. To znamená, že pokud přidělíte velké objekty a chcete se ujistit, že se neposouvou, měli byste je připnout.

Obrázek 1 znázorňuje scénář, kdy uvolňování paměti tvoří generaci 1 po uvolňování paměti první generace 0, kde a jsou zachované, a tvoří generaci 2 po uvolňování paměti první generace 1, kde a Obj1 jsou neschová. Obj3 Obj2 Obj5 Všimněte si, že tyto a následující obrázky jsou pouze ilustrační. Obsahují jen velmi málo objektů, aby lépe ukázaly, co se na haldě děje. Ve skutečnosti je do uvolňování paměti obvykle zapojeno mnoho dalších objektů.

Obrázek 1: Uvolňování paměti gen 0 a uvolňování paměti gen 1
Obrázek 1: Generace 0 a uvolňování paměti 1. generace

Obrázek 2 ukazuje, že po uvolňování paměti 2. generace, které zjistily, že a jsou zachované, tvoří souvislé volné místo z paměti, které bylo zabírána a , které se pak použily ke splnění požadavku na přidělení pro Obj1 Obj2 Obj1 Obj2 Obj4 . Mezera za posledním objektem na konec segmentu se také může použít ke splnění Obj3 požadavků na přidělení.

Obrázek 2: Po gc 2. generace
Obrázek 2: Po uvolňování paměti 2. generace

Pokud není dostatek volného místa pro požadavky na přidělení velkých objektů, globální katalog se nejprve pokusí z operačního systému získat další segmenty. Pokud dojde k selhání, aktivuje uvolňování paměti generace 2 v na paměti, že se nějaké místo zkrátí.

Během uvolňování paměti generace 1 nebo generace 2 uvolňování paměti uvolní systém uvolňování paměti segmenty, které v nich nemají žádné živé objekty, zpět do operačního systému voláním funkce VirtualFree. Místo za posledním živým objektem na konci segmentu se decommitted (s výjimkou dočasného segmentu, kde gen0/gen1 live, kde systém uvolňování paměti nezachová nějaké potvrzené, protože aplikace se do něj bude přidělovat hned). A volné prostory zůstanou potvrzené i po resetování, což znamená, že operační systém nemusí zapisovat data do nich zpět na disk.

Vzhledem k tomu, že se LOH shromažďuje pouze během generace 2 GC, může být segment LOH během tohoto uvolňování paměti volný. Obrázek 3 znázorňuje scénář, kdy systém uvolňování paměti uvolní jeden segment (segment 2) zpět do operačního systému a uvolní více místa ve zbývajících segmentech. Pokud potřebuje použít decommitted prostor na konci segmentu ke splnění požadavků na přidělení velkých objektů, znovu potvrdí paměť. (Vysvětlení commit/decommit najdete v dokumentaci pro VirtualAlloc.

Obrázek 3: LOH po gc 2. generace
Obrázek 3: LOH po uvolňování paměti 2. generace

Kdy se shromažďuje velký objekt?

Obecně platí, že uvolňování paměti probíhá za jedné z následujících tří podmínek:

  • Přidělení překračuje prahovou hodnotu 0. generace nebo velkého objektu.

    Prahová hodnota je vlastnost generování. Prahová hodnota pro generování se nastaví, když do něj systém uvolňování paměti přidělí objekty. Při překročení prahové hodnoty se při této generaci aktivuje uvolňování paměti. Když přidělíte malé nebo velké objekty, spotřebujete 0. generace a prahové hodnoty LOH v uvedeném pořadí. Když systém uvolňování paměti přidělí 1. a 2. generaci, spotřebovává prahové hodnoty. Tyto prahové hodnoty se dynamicky ladí při spuštění programu.

    To je typický případ. k většině zásad skupiny dochází kvůli přidělení na spravované haldě.

  • Je GC.Collect volána metoda .

    Pokud je volána metoda bez parametrů nebo je jako argument předáno jiné přetížení, je LOH shromážděn spolu se zbytkem GC.Collect() GC.MaxGeneration spravované haldy.

  • Systém je v situaci s nedostatek paměti.

    K tomu dojde, když systém uvolňování paměti obdrží oznámení o vysoké paměti z operačního systému. Pokud si systém uvolňování paměti myslí, že uvolňování paměti 2. generace bude produktivní, aktivuje ho.

Vliv na výkon LOH

Přidělení haldy velkého objektu ovlivňuje výkon následujícími způsoby.

  • Náklady na přidělení.

    CLR zaručuje, že paměť pro každý nový objekt, který předá, je vymazána. To znamená, že náklady na přidělení velkého objektu se ovládají vymazáním paměti (pokud nespouštějí uvolňování paměti). Pokud vymazání jednoho byte trvá dva cykly, trvá vymazání nejmenšího velkého objektu 170 000 cyklů. Vymazání paměti 16MB objektu na počítači s 2 GHz trvá přibližně 16 ms. To jsou poměrně velké náklady.

  • Náklady na kolekci.

    Vzhledem k tomu, že se LOH a generace 2 shromažďují společně, při překročení prahové hodnoty jedné z nich se aktivuje kolekce 2. generace. Pokud se kvůli LOH aktivuje kolekce 2. generace, generace 2 nemusí být po uvolňování paměti nutně mnohem menší. Pokud generace 2 nemá moc dat, má to minimální dopad. Pokud je ale generace 2 velká, může dojít k problémům s výkonem, pokud se aktivuje mnoho GC generace 2. Pokud je dočasně přiděleno mnoho velkých objektů a máte velký prostor pro práci s objekty (SOH), můžete být při práci se zásadou skupiny příliš mnoho času. Kromě toho se náklady na přidělení mohou skutečně sčítá, pokud budete dál přidělovat a pouštět opravdu velké objekty.

  • Prvky pole s odkazové typy.

    Velmi velké objekty v LOH jsou obvykle pole (velmi zřídka mají objekt instance, který je ve skutečnosti velký). Pokud jsou prvky pole na úrovni reference, dojde k nepřítomnosti nákladů, které nejsou k dispozici, pokud prvky nejsou na přehledně formátované. Pokud element neobsahuje žádné odkazy, systém uvolňování paměti nemusí projít všemi poli. Například pokud použijete pole k ukládání uzlů v binárním stromu, jedním ze způsobů, jak ho implementovat, je odkazování na pravý a levý uzel uzlu skutečnými uzly:

    class Node
    {
       Data d;
       Node left;
       Node right;
    };
    
    Node[] binary_tr = new Node [num_nodes];
    

    Pokud num_nodes je velká, musí systém uvolňování paměti projít alespoň dvěma odkazy na jeden prvek. Alternativním přístupem je uložit index pravého a levého uzlu:

    class Node
    {
       Data d;
       uint left_index;
       uint right_index;
    } ;
    

    Místo odkazování na data levého uzlu se left.d na něj odkazuje jako na binary_tr[left_index].d . A systém uvolňování paměti nemusí pohlížet na žádné odkazy pro levý a pravý uzel.

Z těchto tří faktorů jsou první dva většinou mnohem významné než třetí. Z tohoto důvodu doporučujeme, abyste přidělili fond velkých objektů, které znovu použijete místo přidělení dočasných.

Shromažďování údajů o výkonu pro LOH

Předtím, než shromáždíte údaje o výkonu konkrétní oblasti, byste už měli provést následující:

  1. Bylo nalezeno legitimace, které byste měli v této oblasti prohledat.

  2. Byly vyčerpány jiné oblasti, o kterých víte, že nenajdete cokoli, co by mohlo vysvětlit problém s výkonem, který jste viděli.

Další informace o základech paměti a procesoru najdete v blogu pochopení problému před tím, než se pokusíte najít řešení.

K shromažďování dat o výkonu LOH můžete použít následující nástroje:

Čítače výkonu paměti .NET CLR

Tyto čítače výkonu jsou obvykle dobrým prvním krokem při zkoumání problémů s výkonem (i když doporučujeme použít události ETW). Nástroj Sledování výkonu konfigurujete tak, že přidáte čítače, které chcete, jak ukazuje obrázek 4. Ty, které jsou relevantní pro LOH, jsou:

  • Kolekce 2. generace

    Zobrazuje počet, kolikrát od spuštění procesu došlo k GC 2. generace. Čítač se zvyšuje na konci kolekce 2. generace (označuje se také jako úplné uvolňování paměti). Tento čítač zobrazuje poslední zjištěnou hodnotu.

  • Velikost haldy Large Object

    Zobrazí aktuální velikost LOH v bajtech, včetně volného místa. Tento čítač se aktualizuje na konci uvolňování paměti, ne při každém přidělení.

Běžný způsob, jak si prohlédnout čítače výkonu, je nástroj Performance Monitor (perfmon.exe). Pomocí možnosti Přidat čítače přidejte zajímavé čítače pro procesy, které vás zajímají. Data čítače výkonu můžete uložit do souboru protokolu, jak ukazuje obrázek 4:

Snímek obrazovky, který ukazuje přidání čítačů výkonu. Obrázek 4: LOH po 2. generaci GC

Čítače výkonu se také dají dotazovat programově. Spousta lidí je shromažďuje tímto způsobem v rámci procesu pravidelného testování. Když vydávají čítače s hodnotami, které jsou z obyčejného, používají jiné prostředky k získání podrobnějších dat, která vám pomohou s šetřením.

Poznámka

Doporučujeme místo čítačů výkonu použít události trasování událostí pro Windows, protože trasování událostí pro Windows poskytuje mnohem rozsáhlejší informace.

Trasování událostí pro Windows – události

Systém uvolňování paměti poskytuje bohatou sadu událostí ETW, které vám pomůžou pochopit, co dělá halda a proč. Následující blogové příspěvky ukazují, jak shromažďovat a pochopit události GC pomocí ETW:

Chcete-li identifikovat nadměrné GC 2. generace způsobené dočasným přidělením LOH, podívejte se do sloupce důvod triggeru pro GC. Pro jednoduchý test, který přiděluje pouze dočasné velké objekty, můžete shromažďovat informace o událostech ETW pomocí následujícího příkazového řádku PerfView :

perfview /GCCollectOnly /AcceptEULA /nogui collect

Výsledek je podobný tomuto:

Snímek obrazovky, který zobrazuje události ETW v PerfView. Obrázek 5: události ETW zobrazené pomocí PerfView

Jak vidíte, všechny GC jsou generace 2 GC a všechny jsou spouštěny v AllocLarge, což znamená, že přidělením velkého objektu aktivovaného tímto GC. Víme, že tato přidělení jsou dočasná, protože sazba LOH pro přežití% kolony uvádí 1%.

Můžete shromažďovat další události ETW, které vám sdělí, kdo tyto velké objekty přidělil. Následující příkazový řádek:

perfview /GCOnly /AcceptEULA /nogui collect

shromažďuje událost AllocationTick, která se aktivuje přibližně pro každé 100 tisíc množství přidělení. Jinými slovy, událost je vyvolána při každém přidělení velkého objektu. Pak se můžete podívat na jedno z zobrazení přidělení haldy GC, které vám ukáže zásobníky volání, že se přidělily velké objekty:

Snímek obrazovky zobrazující zobrazení haldy systému uvolňování paměti. Obrázek 6: zobrazení přidělení haldy GC

Jak vidíte, jedná se o velmi jednoduchý test, který pouze přiděluje velké objekty od své Main metody.

Ladicí program

Pokud je to výpis paměti a potřebujete zjistit, jaké objekty jsou ve skutečnosti na LOH, můžete použít rozšíření ladicího programu SOS , které poskytuje .NET.

Poznámka

příkazy ladění uvedené v této části se vztahují na Windows ladicí programy.

Následující ukázka ukazuje výstup z analýzy LOH:

0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment   begin allocated     size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment   begin allocated     size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT   Count   TotalSize Class Name
001521d0       66     2081792     Free
7912273c       63     6663696 System.Byte[]
7912254c       4     8008736 System.Object[]
Total 133 objects

Velikost haldy LOH je (16 754 224 + 16 699 288 + 16 284 504) = 49 738 016 bajtů. Mezi adresami 023e1000 a 033db630, 8 008 736 bajtů, které jsou obsazeny polem System.Object objektů, 6 663 696 bajty jsou obsazeny polem System.Byte objektů a 2 081 792 bajtů je obsazeno volným místem.

V některých případech ladicí program zobrazí, že celková velikost LOH je menší než 85 000 bajtů. K tomu dochází, protože modul runtime sám používá LOH k přidělení některých objektů, které jsou menší než velký objekt.

Vzhledem k tomu, že se LOH nekomprimuje, někdy se LOH považuje za zdroj fragmentace. Fragmentace znamená:

  • Fragmentace spravované haldy, která je určena množstvím volného místa mezi spravovanými objekty. V SoS !dumpheap –type Free příkaz zobrazí množství volného místa mezi spravovanými objekty.

  • Fragmentace adresního prostoru virtuální paměti (VM), což je paměť označená jako MEM_FREE . Můžete ji získat pomocí různých příkazů ladicího programu v programu WinDbg.

    Následující příklad ukazuje fragmentaci v prostoru virtuálního počítače:

    0:000> !address
    00000000 : 00000000 - 00010000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    00010000 : 00010000 - 00002000
    Type     00020000 MEM_PRIVATE
    Protect 00000004 PAGE_READWRITE
    State   00001000 MEM_COMMIT
    Usage   RegionUsageEnvironmentBlock
    00012000 : 00012000 - 0000e000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    … [omitted]
    -------------------- Usage SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Pct(Busy)   Usage
    701000 (   7172) : 00.34%   20.69%   : RegionUsageIsVAD
    7de15000 ( 2062420) : 98.35%   00.00%   : RegionUsageFree
    1452000 (   20808) : 00.99%   60.02%   : RegionUsageImage
    300000 (   3072) : 00.15%   08.86%   : RegionUsageStack
    3000 (     12) : 00.00%   00.03%   : RegionUsageTeb
    381000 (   3588) : 00.17%   10.35%   : RegionUsageHeap
    0 (       0) : 00.00%   00.00%   : RegionUsagePageHeap
    1000 (       4) : 00.00%   00.01%   : RegionUsagePeb
    1000 (       4) : 00.00%   00.01%   : RegionUsageProcessParametrs
    2000 (       8) : 00.00%   00.02%   : RegionUsageEnvironmentBlock
    Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
    
    -------------------- Type SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    7de15000 ( 2062420) : 98.35%   : <free>
    1452000 (   20808) : 00.99%   : MEM_IMAGE
    69f000 (   6780) : 00.32%   : MEM_MAPPED
    6ea000 (   7080) : 00.34%   : MEM_PRIVATE
    
    -------------------- State SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    1a58000 (   26976) : 01.29%   : MEM_COMMIT
    7de15000 ( 2062420) : 98.35%   : MEM_FREE
    783000 (   7692) : 00.37%   : MEM_RESERVE
    
    Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
    

Je častěji vidět, že fragmentace virtuálního počítače je způsobená dočasnými velkými objekty, které vyžadují, aby systém uvolňování paměti často získal nové spravované segmenty haldy z operačního systému a uvolnil prázdné hodnoty zpátky do operačního systému.

Chcete-li ověřit, zda LOH způsobuje fragmentaci virtuálního počítače, můžete nastavit zarážku na VirtualAlloc a VirtualFree , aby bylo možné zjistit, kdo je zavolal. Například chcete-li zjistit, kdo se pokusil přidělit bloky virtuální paměti větší než 8 MB z operačního systému, můžete nastavit zarážku takto:

bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"

Tento příkaz se zablokuje do ladicího programu a zobrazí zásobník volání, pokud je metoda VirtualAlloc volána s velikostí alokace větší než 8 MB (0x800000).

CLR 2,0 přidal funkci s názvem VM hoarding , která může být užitečná pro scénáře, kdy se často získávají a uvolňují segmenty (včetně v haldách velkých a malých objektů). Pokud chcete zadat hoarding virtuálního počítače, zadejte spouštěcí příznak nazvaný STARTUP_HOARD_GC_VM prostřednictvím hostujícího rozhraní API. Místo uvolnění prázdných segmentů zpět do operačního systému modul CLR zruší v těchto segmentech paměť a umístí je do úsporného seznamu. (Všimněte si, že modul CLR to neudělá pro segmenty, které jsou příliš velké.) Modul CLR později použije tyto segmenty k uspokojení požadavků na nové segmenty. Až aplikace příště potřebuje nový segment, použije CLR jeden z těchto seznamů v pohotovostním režimu, pokud může najít dostatečně velký.

Hoarding virtuálního počítače je také užitečné pro aplikace, které chcete umístit na segmenty, které již získali, například některé serverové aplikace, které jsou dominantní aplikace spuštěné v systému, aby se zamezilo výjimkám nedostatku paměti.

Důrazně doporučujeme, abyste aplikaci pečlivě otestovali při použití této funkce, abyste zajistili, že má vaše aplikace poměrně stabilní využití paměti.