Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
.NET garbage collector (GC) rozdělí objekty na malé a velké objekty. Pokud je objekt velký, některé jeho atributy se stanou významnější než v případě, že je objekt malý. Kompaktnější provedení, to znamená kopírování v paměti jinde na haldě, může být například nákladné. Z tohoto důvodu umístí systém uvolňování paměti velké objekty na hromadu velkých objektů (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 práce garbage collectoru.
Garbage kolektor 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 alokace přidružené k jednotlivým požadavkům měly být po dokončení požadavku uvolněny. Žá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 déle žijícími oblastmi objektů.
Nově přidělené objekty tvoří novou generaci objektů a implicitně generují 0 kolekcí. Pokud se ale jedná o velké objekty, jsou umístěny 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 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. Když dojde ke GC druhé generace, shromažďuje se celá halda. Z tohoto důvodu se generace 2 GC také nazývá full GC. Tento článek pojednává o GC generace 2 místo plného GC, ale termíny jsou zaměnitelné.
Generace poskytují logické zobrazení haldy GC. Fyzicky se objekty nacházejí ve spravovaných segmentech haldy. Spravovaný segment haldy je blok paměti, který si GC rezervuje od operačního systému a volá funkci VirtualAlloc pro spravovaný kód. 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 zpracovávány (v menších blocích), jak je na ně přidělováno stále 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 přeživší z LOH jsou objekty LOH (které jsou shromažďovány spolu s generací 2).
Uživatelský kód může přidělit paměť pouze v generaci 0 (pro malé objekty) nebo v LOH (pro 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 spuštění uvolňování paměti trasuje GC živé objekty a zhušťuje je. Ale protože komprimace je nákladná, GC uklidí LOH; vytvoří bezplatný seznam z 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 spojeny do jednoho volného objektu.
.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 během příštího úplného blokového uvolňování paměti zkompaktován. A v budoucnu se může .NET rozhodnout, že automaticky zkompaktní 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ů.
Obrázek 1: Generace 0 a generace 1 GC.
Obrázek 2 ukazuje, že po generaci 2 GC, která zjistila, že Obj1 a Obj2 jsou již nepoužívaní, GC vytvořilo souvislý volný prostor z paměti, kterou dříve okupovaly Obj1 a Obj2, a tento prostor byl poté použit k uspokojení žádosti o přidělení pro Obj4. Mezera za posledním objektem, Obj3 a konec segmentu lze také použít k uspokojení žádostí o přidělení.
Obrázek 2: Po druhé generaci 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 generaci 2 GC v naději na uvolnění nějakého místa.
Během generace 1 nebo generace 2 uvolňování paměti garbage collector 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 až ke konci segmentu je uvolněna (s výjimkou dočasného segmentu, kde žijí generace gen0/gen1, kde uvolňovač paměti ponechává některé části rezervované, protože vaše aplikace v něm bude okamžitě alokovat paměť). 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 garbage collector uvolní jeden segment (segment 2) zpět do operačního systému a dekomituje více místa u zbývajících segmentů. Pokud je potřeba použít uvolněný prostor na konci úseku, aby byly uspokojeny velké požadavky na přidělení objektů, znovu přidělí paměť. (Vysvětlení potvrzení/dekommitu najdete v dokumentaci pro VirtualAlloc.)
Obrázek 3: LOH po generaci 2 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ělování překračuje prahovou hodnotu generace 0 nebo prahovou hodnotu pro velké objekty.
Prahová hodnota je vlastnost generace. Prahová hodnota pro generaci je nastavena, když garbage collector přiděluje objekty do ní. Při překročení prahové hodnoty se v této generaci aktivuje GC. Když přidělíte malé nebo velké objekty, spotřebováváte prahovou hodnotu 0. generace pro malé objekty a prahovou hodnotu LOH pro velké objekty. Když garbage collector alokuje do generace 1 a 2, spotřebovává 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 metoda GC.Collect.
Pokud je volána metoda bez GC.Collect() parametrů nebo je předáno jiné přetížení jako argument GC.MaxGeneration, LOH se shromažďuje spolu se zbytkem spravované haldy.
Systém je v situaci s nedostatkem paměti.
K tomu dochází, když garbage collector obdrží upozornění o vysoké paměťové zátěži od operačního systému. Pokud garbage collector uzná, že provedení generaci 2 v GC bude produktivní, aktivuje ho.
Dopady výkonu LOH
Přidělení na haldě velkých objektů ovlivňují výkon následujícími způsoby.
Náklady na přidělení.
CLR zaručuje, že paměť poskytnutá pro každý nový objekt 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 inkaso.
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 GC 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í mohou skutečně zvyšovat, pokud budete dál přidělovat a uvolňovat opravdu velké objekty.
Prvky pole s referenční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 odkazy, vznikají náklady, které nejsou, pokud prvky nejsou bohaté na odkazy. Pokud prvek neobsahuje žádné odkazy, garbage collector (systém uvolňování paměti) nemusí pole vůbec procházet. 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_nodesje 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 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 jako
left.dna něj odkazujete jakobinary_tr[left_index].d. Garbage collector nemusí zkoumat žá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í:
- Našli jste důkaz, že byste se měli podívat na tuto oblast.
- Prozkoumali jste všechny další oblasti, o kterých víte, ale nenašli jste nic, co by mohlo vysvětlit problém s výkonem, který jste zaznamenali.
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
Paměťové čítače výkonu .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 Windows ETW události). Běžným způsobem, jak se podívat na čítače výkonu, je pomocí nástroje Performance Monitor (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ů generace 2 GC od začátku procesu. Čítač se inkrementuje na konci kolekce generace 2 (také se nazývá úplná garbage kolekce). 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 garbage kolekce, nikoli při každém přidělení.
Výkonové čítače můžete také programaticky dotazovat použitím třídy PerformanceCounter. Pro LOH zadejte ".NET CLR Memory" jako CategoryName a "Large Object Heap 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é programově shromažďovat čítače. Když zjistíte čítače s hodnotami, které jsou mimo normu, použijte jiné prostředky k získání podrobnějších dat, která vám pomohou s vyšetřováním.
Poznámka:
Doporučujeme používat události ETW místo čítačů výkonu, protože ETW poskytuje mnohem bohatší informace.
Události ETW
Správce paměti poskytuje bohatou sadu událostí ETW, které vám pomohou pochopit, co se děje s haldou a proč. Následující blogové příspěvky ukazují, jak shromažďovat a porozumět GC událostem pomocí ETW:
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, vám umožní shromažďovat informace o událostech ETW pomocí následujícího příkazu PerfView:
perfview /GCCollectOnly /AcceptEULA /nogui collect
Výsledek je podobný tomuto:
Jak vidíte, všechny GC jsou generace 2 a všechny jsou spuštěny mechanismem AllocLarge, což znamená, že přidělení velkého objektu spustilo tento GC. Víme, že tato přidělení jsou dočasná, protože sloupec míra přežití LOH % uvádí 1%.
Můžete shromažďovat další události ETW, 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 GC, která vám ukazují zásobníky volání, jež přidělily velké objekty.
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 pouze výpis paměti a potřebujete zjistit, jaké objekty jsou v LOH, můžete použít rozšíření ladicího programu SoS dostupné v rozhraní .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.
Někdy debugger 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 Freepří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ů pro ladění 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í paměti způsobené 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é paměťové haldy a vracel 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 přejde do ladicího programu a zobrazí zásobník volání pouze v případě, že je volána 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, kde se segmenty (včetně hald velkých a malých objektů) často získávají a uvolňují. 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ý.
Hromadění virtuálních počítačů je také užitečné pro aplikace, které chtějí udržet již získané segmenty, například některé serverové aplikace dominující 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.