Velká halda objektu v systémech Windows

Uvolňování paměti .NET (GC) rozdělí objekty do malých a velkých objektů. Pokud je objekt velký, některé jeho atributy se stanou významnější než v případě, že je objekt malý. Komprimace může být například nákladná , tj. kopírování v paměti jinde na haldě. Z tohoto důvodu systém uvolňování paměti umístí velké objekty na haldu velkého objektu (LOH). Tento článek popisuje, co kvalifikuje objekt jako velký objekt, jak se shromažďují velké objekty a jaký druh výkonu mají velké objekty.

Důležité

Tento článek popisuje velkou haldu objektů v rozhraní .NET Framework a .NET Core běžící pouze v systémech Windows. 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 velikosti 85 000 bajtů, považuje se za velký objekt. Toto číslo bylo určeno laděním výkonu. Pokud je požadavek na přidělení objektu 85 000 nebo více bajtů, modul runtime ho přidělí na haldě velkého objektu.

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

Uvolňování paměti je generační kolektor. Má tři generace: generaci 0, generaci 1 a generaci 2. Důvodem pro tři generace je, že v dobře vyladěné aplikaci většina objektů zemře v gen0. Například v serverové aplikaci by přidělení přidružená k jednotlivým požadavkem měla po dokončení požadavku zemřít. Žádosti o přidělení v letu ho převedou do gen1 a zemře tam. Gen1 v podstatě funguje jako vyrovnávací paměť mezi mladými oblastmi objektů a dlouhodobými oblastmi objektů.

Nově přidělené objekty tvoří novou generaci objektů a implicitně generují 0 kolekcí. Pokud jsou ale velké objekty, jdou na velkou haldu objektu (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 jsou shromažďovány pouze během kolekce 2. generace. Když se shromažďuje generace, shromažďují se také všechny její mladší generace. Například když dojde ke generování 1 GC, shromažďují se obě generace 1 a 0. A když se stane generace 2 GC, shromažďuje se celá halda. Z tohoto důvodu se generace 2 GC také nazývá úplné uvolňování paměti. Tento článek se týká generace 2 GC místo plného uvolňování paměti, ale termíny jsou zaměnitelné.

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

Žádosti o přidělení jsou pak splněny umístěním spravovaných objektů do těchto spravovaných segmentů haldy. Pokud je objekt menší než 85 000 bajtů, umístí se do segmentu soH; jinak se umístí do segmentu LOH. Segmenty jsou potvrzeny (v menších blocích), protože do nich je přiděleno více a více objektů. Pro SOH jsou objekty, které přežijí GC, povýšeny na další generaci. Objekty, které přežijí kolekci generace 0, se nyní považují za 1. generace a tak dále. Objekty, které přežijí nejstarší generaci, se však stále považují za nejstarší generaci. Jinými slovy, přeživší z generace 2 jsou objekty generace 2; a pozůstalí z LOH jsou objekty LOH (které jsou shromažďovány s gen2).

Uživatelský kód může přidělit pouze v generaci 0 (malé objekty) nebo LOH (velké objekty). Pouze GC může "přidělit" objekty v generaci 1 (podporou přeživších z generace 0) a generace 2 (podporou přeživších z generace 1).

Při aktivaci uvolňování paměti trasuje uvolňování paměti živé objekty a zkomprimuje je. Vzhledem k tomu, že komprimace je nákladná, GC uklidí LOH; vytvoří bezplatný seznam mrtvých objektů, které lze později znovu použít k uspokojení požadavků na přidělení velkých objektů. Sousední mrtvé objekty jsou tvořeny jedním volným objektem.

.NET Core a .NET Framework (počínaje rozhraním .NET Framework 4.5.1) zahrnují GCSettings.LargeObjectHeapCompactionMode vlastnost, která uživatelům umožňuje určit, že LOH by měl být komprimován během dalšího úplného blokování uvolňování paměti. A v budoucnu se .NET může rozhodnout, že se automaticky zkomprimuje LOH. To znamená, že pokud přidělíte velké objekty a chcete se ujistit, že se nepřesunou, měli byste je pořád připnout.

Obrázek 1 znázorňuje scénář, kdy GC tvoří generaci 1 po první generaci 0 GC, kde Obj1 a Obj3 jsou mrtví, a tvoří generaci 2 po první generaci 1 GC, kde Obj2 a Obj5 jsou mrtví. Všimněte si, že tento a následující obrázky jsou určené pouze pro ilustraci; obsahují velmi málo objektů, aby lépe ukázaly, co se stane na haldě. Ve skutečnosti je v GC obvykle zapojeno mnoho dalších objektů.

Figure 1: A gen 0 GC and a gen 1 GC
Obrázek 1: Generace 0 a generace 1 GC.

Obrázek 2 ukazuje, že po generaci 2 GC, která viděla, že Obj1 a Obj2 jsou mrtví, GC tvoří souvislý volný prostor z paměti, který byl použit Obj1Obj2k uspokojení žádosti o přidělení pro Obj4. Mezeru za posledním objektem Obj3, na konec segmentu lze také použít k uspokojení žádostí o přidělení.

Figure 2: After a gen 2 GC
Obrázek 2: Po generování 2 GC

Pokud není dostatek volného místa pro přizpůsobení požadavků na přidělení velkých objektů, GC se nejprve pokusí získat více segmentů z operačního systému. Pokud se to nezdaří, aktivuje generování 2 GC v naději, že uvolní nějaké místo.

Během generace 1 nebo generace 2 uvolňování paměti uvolňování paměti uvolní segmenty, které nemají živé objekty zpět do operačního systému voláním funkce VirtualFree. Mezera za posledním živým objektem na konec segmentu je dekomenzována (s výjimkou dočasného segmentu, kde gen0/gen1 živě, kde uvolňování paměti zachová určité potvrzené, protože vaše aplikace v něm bude okamžitě přidělena). A volné prostory zůstanou potvrzené, i když se resetují, což znamená, že operační systém nemusí zapisovat data zpět na disk.

Vzhledem k tomu, že LOH se shromažďuje pouze během generace 2 GCS, lze segment LOH uvolnit pouze během takového GC. Obrázek 3 znázorňuje scénář, kdy uvolňování paměti uvolní jeden segment (segment 2) zpět do operačního systému a dekomotuje více místa u zbývajících segmentů. Pokud potřebuje použít dekomgenerovaný prostor na konci segmentu, aby uspokojil velké požadavky na přidělení objektů, potvrdí paměť znovu. (Vysvětlení potvrzení/demmitu najdete v dokumentaci pro VirtualAlloc.)

Figure 3: LOH after a gen 2 GC
Obrázek 3: LOH po 2. generaci GC

Kdy se shromažďuje velký objekt?

Obecně platí, že GC probíhá za jedné z následujících tří podmínek:

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

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

    Toto je typický případ; většina GC se stává kvůli přidělení spravované haldy.

  • Volá se GC.Collect metoda.

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

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

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

Vliv na výkon LOH

Přidělení velké haldy objektu ovlivňují 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ý poskytuje, je vymazána. To znamená, že náklady na přidělení velkého objektu jsou dominovány vymazáním paměti (pokud nespustí GC). Pokud k vymazání jednoho bajtu trvá dva cykly, trvá 170 000 cyklů k vymazání nejmenšího velkého objektu. Vymazání paměti objektu 16 MB 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ě, je-li překročena prahová hodnota jedné z nich, aktivuje se kolekce generace 2. Pokud je kolekce generace 2 aktivována kvůli LOH, generace 2 nemusí být po uvolňování paměti mnohem menší. Pokud v generaci 2 není mnoho dat, má to minimální dopad. Pokud je ale generace 2 velká, může to způsobit problémy s výkonem, pokud se aktivuje mnoho GCS generace 2. Pokud je na dočasném základě přiděleno mnoho velkých objektů a máte velký SOH, můžete strávit příliš mnoho času prováděním GCS. Kromě toho se náklady na přidělení můžou ve skutečnosti sčítat, pokud budete dál přidělovat a nechat jít opravdu velkými objekty.

  • Prvky pole s odkazovými typy

    Velmi velké objekty v LOH jsou obvykle pole (je velmi vzácné mít objekt instance, který je opravdu velký). Pokud jsou prvky pole bohaté na odkaz, účtují se náklady, které nejsou k dispozici, pokud prvky nejsou bohaté na odkaz. Pokud prvek neobsahuje žádné odkazy, systém uvolňování paměti nemusí vůbec procházet polem. Pokud například použijete pole k ukládání uzlů do binárního stromu, jedním ze způsobů, jak ho implementovat, je odkazovat 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ý, systém uvolňování paměti musí projít alespoň dvěma odkazy na prvek. Alternativním přístupem je uložení indexu zprava a levého uzlu:

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

    Místo odkazování na data levého uzlu jako left.dna něj odkazujete jako binary_tr[left_index].d. Systém uvolňování paměti nemusí hledat žádné odkazy na levý a pravý uzel.

Z těchto tří faktorů jsou první dva obvykle významnější než třetí. Z tohoto důvodu doporučujeme přidělit fond velkých objektů, které znovu použijete místo přidělování dočasných objektů.

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

Před shromažďováním údajů o výkonu pro konkrétní oblast byste už měli provést následující:

  1. Našli jste důkaz, že byste se měli podívat na tuto oblast.
  2. Vyčerpání dalších oblastí, o kterých víte, aniž byste našli 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 Vysvětlení problému předtím, než se pokusíte najít řešení.

Ke 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

Čítače výkonupamětich technologie .NET CLR jsou obvykle dobrým prvním krokem při vyšetřování problémů s výkonem (i když doporučujeme používat události Trasování událostí pro Windows). Běžným způsobem, jak se podívat na čítače výkonu, je Sledování výkonu (perfmon.exe). Vyberte Přidat (Ctrl + A) a přidejte zajímavé čítače pro procesy, o které vám záleží. Data čítače výkonu můžete uložit do souboru protokolu.

Následující dva čítače v kategorii paměti .NET CLR jsou relevantní pro LOH:

  • # Kolekce Gen 2

    Zobrazí počet výskytů 2GC generace od spuštění procesu. Čítač se zvýší na konci kolekce generace 2 (označuje se také jako úplné uvolňování paměti). Tento čítač zobrazí poslední pozorovanou hodnotu.

  • Velikost haldy velkého objektu

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

Screenshot that shows adding counters in Performance Monitor.

Čítače výkonu PerformanceCounter můžete dotazovat také programově pomocí třídy. Pro LOH zadejte ".NET CLR Memory" jako CategoryName a "Large Object Hald size" (Velikost haldy velkého objektu) jako .CounterName

PerformanceCounter performanceCounter = new()
{
    CategoryName = ".NET CLR Memory",
    CounterName = "Large Object Heap size",
    InstanceName = "<instance_name>"
};

Console.WriteLine(performanceCounter.NextValue());

V rámci procesu rutinního testování je běžné shromažďovat čítače prostřednictvím kódu programu. Když zjistíte čítače s hodnotami, které jsou mimo běžnou hodnotu, použijte jiné prostředky k získání podrobnějších dat, které vám pomůžou s šetřením.

Poznámka:

Místo čítačů výkonu doporučujeme používat události Trasování událostí pro Windows, protože EtW poskytuje mnohem bohatší informace.

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

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

Pokud chcete zjistit nadměrné množství GCS generace 2 způsobené dočasnými přiděleními LOH, projděte si sloupec Důvod aktivační události pro GCS. Jednoduchý test, který přiděluje pouze dočasné velké objekty, můžete shromažďovat informace o událostech Trasování událostí pro Windows pomocí následujícího příkazu PerfView :

perfview /GCCollectOnly /AcceptEULA /nogui collect

Výsledek je podobný tomuto:

Screenshot that shows ETW events in PerfView.

Jak vidíte, všechny GCS jsou generace 2 GCS a všechny jsou aktivovány AllocLarge, což znamená, že přidělení velkého objektu aktivovalo tento GC. Víme, že tato přidělení jsou dočasná, protože sloupec LOH Survival Rate % říká 1 %.

Můžete shromažďovat další události Trasování událostí pro Windows, které vám řeknou, 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ě každých 100 tisíc přidělení. Jinými slovy, událost se aktivuje při každém přidělení velkého objektu. Pak se můžete podívat na jedno z zobrazení haldy haldy, které vám ukážou volání, která přidělila velké objekty:

Screenshot that shows a garbage collector heap view.

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

Ladicí program

Pokud máte jen výpis paměti a potřebujete se podívat, jaké objekty jsou ve skutečnosti v LOH, můžete použít rozšíření ladicího programu SoS poskytované rozhraním .NET.

Poznámka:

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

Následující příklad ukazuje ukázkový výstup 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ů je obsazeno polem System.Object objektů, 6 663 696 bajtů je obsazeno polem System.Byte objektů a 2 081 792 bajtů jsou obsazeny volným místem.

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

Vzhledem k tomu, že LOH není komprimován, někdy je LOH považován za zdroj fragmentace. Fragmentace znamená:

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

  • Fragmentace adresního prostoru virtuální paměti, což je paměť označená jako MEM_FREE. Můžete ho získat pomocí různých příkazů ladicího programu ve 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)
    

Častěji dochází k fragmentaci virtuálních počítačů způsobených dočasnými velkými objekty, které vyžadují, aby systém uvolňování paměti často získával z operačního systému nové segmenty spravované haldy a vyvolal prázdné segmenty zpět do operačního systému.

Pokud chcete ověřit, jestli LOH způsobuje fragmentaci virtuálních počítačů, můžete nastavit zarážku na VirtualAlloc a VirtualFree a zjistit, kdo je volal. Pokud chcete například 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 rozdělí do ladicího programu a zobrazí zásobník volání pouze v případě , že se volá VirtualAlloc s velikostí přidělení větší než 8 MB (0x800000).

CLR 2.0 přidal funkci nazvanou VM Hoarding , která může být užitečná pro scénáře, kdy se často získávají a vydávají segmenty (včetně velkých a malých hald objektů). Pokud chcete zadat Hoarding virtuálního počítače, zadejte příznak spuštění volaný STARTUP_HOARD_GC_VM prostřednictvím hostitelského rozhraní API. Místo uvolnění prázdných segmentů zpět do operačního systému clR dekomituje paměť v těchto segmentech a umístí je do pohotovostního seznamu. (Všimněte si, že CLR to nedělá u segmentů, které jsou příliš velké.) CLR později tyto segmenty použije k uspokojení nových požadavků na segmenty. Když příště vaše aplikace potřebuje nový segment, modul CLR použije jeden z tohoto pohotovostního seznamu, pokud najde dostatečně velký.

Virtuální počítače jsou také užitečné pro aplikace, které chtějí držet segmenty, které už získaly, například některé serverové aplikace, které jsou dominantními aplikacemi spuštěnými v systému, aby se zabránilo výjimkám z nedostatku paměti.

Důrazně doporučujeme, abyste aplikaci pečlivě testovala, když tuto funkci používáte, abyste měli jistotu, že vaše aplikace má poměrně stabilní využití paměti.