De grote object heap op Windows-systemen
De .NET garbage collector (GC) verdeelt objecten in kleine en grote objecten. Wanneer een object groot is, worden sommige kenmerken belangrijker dan als het object klein is. Het comprimeren van het bestand ( dat wil gezegd, het kopiëren in het geheugen ergens anders op de heap) kan bijvoorbeeld duur zijn. Hierdoor plaatst de garbagecollection grote objecten op de grote object heap (LOH). In dit artikel wordt besproken wat een object in aanmerking komt als een groot object, hoe grote objecten worden verzameld en wat voor soort prestatie-implicaties grote objecten opleggen.
Belangrijk
In dit artikel worden de grote object-heap in .NET Framework en .NET Core alleen op Windows-systemen besproken. Het omvat niet de LOH die wordt uitgevoerd op .NET-implementaties op andere platforms.
Hoe een object eindigt op de LOH
Als een object groter is dan of gelijk is aan 85.000 bytes, wordt dit beschouwd als een groot object. Dit aantal is bepaald door het afstemmen van de prestaties. Wanneer een objecttoewijzingsaanvraag voor 85.000 of meer bytes is, wijst de runtime deze toe aan de grote object-heap.
Om te begrijpen wat dit betekent, is het handig om enkele basisprincipes van de garbagecollector te onderzoeken.
De garbagecollection is een generatieverzamelaar. Het heeft drie generaties: generatie 0, generatie 1 en generatie 2. De reden voor het hebben van drie generaties is dat in een goed afgestemde app de meeste objecten sterven in gen0. In een server-app moeten de toewijzingen die aan elke aanvraag zijn gekoppeld, bijvoorbeeld sterven nadat de aanvraag is voltooid. De in-flight toewijzingsaanvragen komen in gen1 en sterven daar. Gen1 fungeert in wezen als buffer tussen jonge objectgebieden en langlevende objectgebieden.
Nieuw toegewezen objecten vormen een nieuwe generatie objecten en zijn impliciet verzamelingen van de 0e generatie. Als ze echter grote objecten zijn, gaan ze op de grote object heap (LOH), die soms generatie 3 wordt genoemd. Generatie 3 is een fysieke generatie die logisch wordt verzameld als onderdeel van generatie 2.
Grote objecten behoren tot generatie 2 omdat ze alleen tijdens een verzameling van de tweede generatie worden verzameld. Wanneer een generatie wordt verzameld, worden ook alle jongere generatie(s) verzameld. Wanneer bijvoorbeeld een GC van de eerste generatie plaatsvindt, worden zowel generatie 1 als 0 verzameld. En wanneer een generatie 2 GC gebeurt, wordt de hele heap verzameld. Daarom wordt een generatie 2 GC ook wel een volledige GC genoemd. Dit artikel verwijst naar generatie 2 GC in plaats van volledige GC, maar de termen zijn uitwisselbaar.
Generaties bieden een logisch beeld van de GC-heap. Fysiek leven objecten in beheerde heap-segmenten. Een beheerd heap-segment is een deel van het geheugen dat de GC van het besturingssysteem reserveert door de functie VirtualAlloc namens beheerde code aan te roepen. Wanneer de CLR wordt geladen, wijst de GC twee eerste heapsegmenten toe: één voor kleine objecten (de kleine object heap of SOH) en één voor grote objecten (de grote object heap).
De toewijzingsaanvragen worden vervolgens voldaan door beheerde objecten op deze beheerde heap-segmenten te plaatsen. Als het object kleiner is dan 85.000 bytes, wordt het object in het segment voor de SOH geplaatst; anders wordt het in een LOH-segment geplaatst. Segmenten worden vastgelegd (in kleinere segmenten) omdat er steeds meer objecten aan worden toegewezen. Voor de SOH worden objecten die een GC overleven gepromoveerd tot de volgende generatie. Objecten die een verzameling van de 0e generatie overleven, worden nu beschouwd als objecten van de eerste generatie, enzovoort. Objecten die de oudste generatie overleven, worden echter nog steeds beschouwd als de oudste generatie. Met andere woorden, overlevenden van generatie 2 zijn generatie 2 objecten; en overlevenden van de LOH zijn LOH-objecten (die met gen2 worden verzameld).
Gebruikerscode kan alleen worden toegewezen in generatie 0 (kleine objecten) of de LOH (grote objecten). Alleen de GC kan objecten in generatie 1 "toewijzen" (door overlevenden van generatie 0 te bevorderen) en generatie 2 (door overlevenden van generatie 1 te bevorderen).
Wanneer een garbagecollection wordt geactiveerd, traceert de GC de live-objecten en compacteert deze. Maar omdat compressie duur is, veegt de GC de LOH; het maakt een gratis lijst van dode objecten die later opnieuw kunnen worden gebruikt om te voldoen aan grote objecttoewijzingsaanvragen. Aangrenzende dode objecten worden gemaakt in één vrij object.
.NET Core en .NET Framework (beginnend met .NET Framework 4.5.1) bevatten de GCSettings.LargeObjectHeapCompactionMode eigenschap waarmee gebruikers kunnen opgeven dat de LOH moet worden gecomprimeerd tijdens de volgende volledige blokkerende GC. En in de toekomst kan .NET besluiten om de LOH automatisch te comprimeren. Dit betekent dat als u grote objecten toewijst en er zeker van wilt zijn dat ze niet worden verplaatst, u ze nog steeds vast moet maken.
Afbeelding 1 illustreert een scenario waarin de GC de eerste generatie 0 GC vormt waar Obj1
en Obj3
dood is, en het de tweede generatie GC vormt na de eerste generatie 1 GC waar Obj2
en Obj5
dood zijn. Houd er rekening mee dat deze en de volgende cijfers alleen ter illustratie zijn bedoeld; ze bevatten zeer weinig objecten om beter te laten zien wat er gebeurt op de heap. In werkelijkheid zijn er veel meer objecten betrokken bij een GC.
Afbeelding 1: Een generatie 0 en een generatie 1 GC.
Afbeelding 2 toont aan dat na een generatie 2 GC die zag dat Obj1
en dood zijn, de GC aaneengesloten vrije ruimte uit het geheugen vormt die vroeger bezet was en Obj1
Obj2
, die vervolgens werd gebruikt om te voldoen aan een toewijzingsaanvraag voor Obj4
Obj2
. De ruimte na het laatste object, Obj3
tot het einde van het segment, kan ook worden gebruikt om te voldoen aan toewijzingsaanvragen.
Afbeelding 2: Na een generatie 2 GC
Als er onvoldoende vrije ruimte is om tegemoet te komen aan de aanvragen voor grote objecttoewijzingen, probeert de GC eerst meer segmenten van het besturingssysteem te verkrijgen. Als dat mislukt, activeert het een generatie 2 GC in de hoop om wat ruimte vrij te maken.
Tijdens een generatie 1 of 2 GC brengt de garbagecollector segmenten vrij die geen liveobjecten hebben terug naar het besturingssysteem door de functie VirtualFree aan te roepen. Ruimte na het laatste live-object aan het einde van het segment wordt gedecommitteerd (behalve in het kortstondige segment waar gen0/gen1 woont, waarbij de garbagecollector een aantal doorgevoerd houdt omdat uw toepassing er meteen in wordt toegewezen). En de vrije spaties blijven doorgevoerd, hoewel ze opnieuw worden ingesteld, wat betekent dat het besturingssysteem geen gegevens hoeft te schrijven naar de schijf.
Aangezien de LOH alleen wordt verzameld tijdens generatie 2 GC's, kan het LOH-segment alleen worden vrijgemaakt tijdens een dergelijke GC. In afbeelding 3 ziet u een scenario waarin de garbagecollector één segment (segment 2) terugbrengt naar het besturingssysteem en meer ruimte op de resterende segmenten uitsplitst. Als de ruimte die is gedecommitteerd aan het einde van het segment moet worden gebruikt om te voldoen aan grote aanvragen voor objecttoewijzing, wordt het geheugen opnieuw doorgevoerd. (Zie de documentatie voor VirtualAlloc.)
Afbeelding 3: De LOH na een generatie 2 GC
Wanneer wordt een groot object verzameld?
Over het algemeen vindt een GC plaats onder een van de volgende drie voorwaarden:
Toewijzing overschrijdt de drempelwaarde van generatie 0 of groot object.
De drempelwaarde is een eigenschap van een generatie. Een drempelwaarde voor een generatie wordt ingesteld wanneer de garbagecollector objecten eraan toewijst. Wanneer de drempelwaarde wordt overschreden, wordt een GC geactiveerd voor die generatie. Wanneer u kleine of grote objecten toewijst, verbruikt u respectievelijk generatie 0 en de drempelwaarden van LOH. Wanneer de garbagecollector wordt toegewezen aan generatie 1 en 2, verbruikt deze de drempelwaarden. Deze drempelwaarden worden dynamisch afgestemd terwijl het programma wordt uitgevoerd.
Dit is het typische geval; de meeste GCs gebeuren vanwege toewijzingen op de beheerde heap.
De GC.Collect methode wordt aangeroepen.
Als de parameterloze GC.Collect() methode wordt aangeroepen of als argument een andere overbelasting wordt doorgegeven GC.MaxGeneration , wordt de LOH verzameld samen met de rest van de beheerde heap.
Het systeem bevindt zich in een situatie met weinig geheugen.
Dit gebeurt wanneer de garbagecollector een melding met hoog geheugen van het besturingssysteem ontvangt. Als de garbagecollector denkt dat het uitvoeren van een generatie 2 GC productief is, wordt er een geactiveerd.
Gevolgen voor LOH-prestaties
Toewijzingen voor de grote object-heap beïnvloeden de prestaties op de volgende manieren.
Toewijzingskosten.
De CLR maakt de garantie dat het geheugen voor elk nieuw object dat het uitgeeft, wordt gewist. Dit betekent dat de toewijzingskosten van een groot object worden overheerst door het wissen van geheugen (tenzij er een GC wordt geactiveerd). Als er twee cycli nodig zijn om één byte te wissen, duurt het 170.000 cycli om het kleinste grote object te wissen. Het wissen van het geheugen van een object van 16 MB op een 2 GHz-machine duurt ongeveer 16 ms. Dat zijn nogal grote kosten.
Kosten voor het verzamelen.
Omdat de LOH en generatie 2 samen worden verzameld, wordt een verzameling van de tweede generatie geactiveerd als de drempelwaarde van een van beide wordt overschreden. Als een verzameling van de tweede generatie wordt geactiveerd vanwege de LOH, is generatie 2 niet noodzakelijkerwijs veel kleiner na de GC. Als er niet veel gegevens van generatie 2 zijn, heeft dit minimale gevolgen. Maar als generatie 2 groot is, kan dit prestatieproblemen veroorzaken als veel generatie 2 GCs worden geactiveerd. Als veel grote objecten tijdelijk worden toegewezen en u een grote SOH hebt, kunt u te veel tijd besteden aan het uitvoeren van PC's. Bovendien kunnen de toewijzingskosten echt oplopen als u steeds grote objecten toegeeft en loslaat.
Matrixelementen met verwijzingstypen.
Zeer grote objecten op de LOH zijn meestal matrices (het is zeer zeldzaam om een exemplaarobject te hebben dat echt groot is). Als de elementen van een matrix verwijzingsrijk zijn, worden er kosten in rekening gebracht die niet aanwezig zijn als de elementen niet verwijzingsrijk zijn. Als het element geen verwijzingen bevat, hoeft de garbagecollector helemaal niet door de matrix te lopen. Als u bijvoorbeeld een matrix gebruikt om knooppunten op te slaan in een binaire structuur, kunt u deze implementeren door te verwijzen naar het rechter- en linkerknooppunt van een knooppunt door de werkelijke knooppunten:
class Node { Data d; Node left; Node right; }; Node[] binary_tr = new Node [num_nodes];
Als
num_nodes
dit groot is, moet de garbagecollector ten minste twee verwijzingen per element doorlopen. Een alternatieve benadering is het opslaan van de index van de rechter- en linkerknooppunten:class Node { Data d; uint left_index; uint right_index; } ;
In plaats van de gegevens van het linkerknooppunt te verwijzen als
left.d
, verwijst u ernaar alsbinary_tr[left_index].d
. En de garbagecollector hoeft geen verwijzingen naar het linker- en rechterknooppunt te bekijken.
Uit de drie factoren zijn de eerste twee meestal belangrijker dan de derde. Daarom raden we u aan een groep grote objecten toe te wijzen die u opnieuw gebruikt in plaats van tijdelijke objecten toe te wijzen.
Prestatiegegevens verzamelen voor de LOH
Voordat u prestatiegegevens voor een specifiek gebied verzamelt, moet u het volgende al hebben gedaan:
- Er is bewijs gevonden dat u naar dit gebied moet kijken.
- Uitgeputte andere gebieden waarvan u weet zonder iets te vinden dat het prestatieprobleem kan verklaren dat u hebt gezien.
Zie voor meer informatie over de basisprincipes van geheugen en de CPU het blog Begrijpen van het probleem voordat u een oplossing probeert te vinden.
U kunt de volgende hulpprogramma's gebruiken om gegevens over de prestaties van LOH te verzamelen:
Prestatiemeteritems voor .NET CLR-geheugen
Prestatiemeteritems voor .NET CLR-geheugen zijn meestal een goede eerste stap bij het onderzoeken van prestatieproblemen (hoewel we u aanraden ETW-gebeurtenissen te gebruiken). Een veelvoorkomende manier om prestatiemeteritems te bekijken, is met Performance Monitor (perfmon.exe). Selecteer Toevoegen (Ctrl + A) om de interessante tellers toe te voegen voor processen die u belangrijk vindt. U kunt de prestatiemeteritemgegevens opslaan in een logboekbestand.
De volgende twee tellers in de categorie .NET CLR Memory zijn relevant voor de LOH:
# Gen 2-verzamelingen
Geeft het aantal keren weer dat generatie 2 GCs zijn opgetreden sinds het proces is gestart. De teller wordt verhoogd aan het einde van een verzameling van de tweede generatie (ook wel een volledige garbagecollection genoemd). Deze teller geeft de laatst waargenomen waarde weer.
Grote object heap grootte
Geeft de huidige grootte weer in bytes, inclusief vrije ruimte, van de LOH. Deze teller wordt bijgewerkt aan het einde van een garbagecollection, niet bij elke toewijzing.
U kunt ook programmatisch query's uitvoeren op prestatiemeteritems met behulp van de PerformanceCounter klasse. Geef voor de LOH '.NET CLR Memory' op als de CategoryName en 'Large Object Heap size' als de CounterName.
PerformanceCounter performanceCounter = new()
{
CategoryName = ".NET CLR Memory",
CounterName = "Large Object Heap size",
InstanceName = "<instance_name>"
};
Console.WriteLine(performanceCounter.NextValue());
Het is gebruikelijk om tellers programmatisch te verzamelen als onderdeel van een routinetestproces. Wanneer u tellers met waarden ziet die niet normaal zijn, gebruikt u andere middelen om meer gedetailleerde gegevens te krijgen om u te helpen met het onderzoek.
Notitie
We raden u aan ETW-gebeurtenissen te gebruiken in plaats van prestatiemeteritems, omdat ETW veel uitgebreidere informatie biedt.
ETW-gebeurtenissen
De garbagecollection biedt een uitgebreide set ETW-gebeurtenissen om u te helpen begrijpen wat de heap doet en waarom. In de volgende blogberichten ziet u hoe u GC-gebeurtenissen verzamelt en begrijpt met ETW:
- GC ETW-gebeurtenissen - 1
- GC ETW-gebeurtenissen - 2
- GC ETW-gebeurtenissen - 3
- GC ETW-gebeurtenissen - 4
Als u overmatige generatie 2 GCs wilt identificeren die worden veroorzaakt door tijdelijke LOH-toewijzingen, bekijkt u de kolom Triggerreden voor GC's. Voor een eenvoudige test die alleen tijdelijke grote objecten toewijst, kunt u informatie verzamelen over ETW-gebeurtenissen met de volgende PerfView-opdracht :
perfview /GCCollectOnly /AcceptEULA /nogui collect
Het resultaat ziet er ongeveer als volgt uit:
Zoals u ziet, zijn alle GCs generatie 2 GCs en ze worden allemaal geactiveerd door AllocLarge, wat betekent dat het toewijzen van een groot object deze GC heeft geactiveerd. We weten dat deze toewijzingen tijdelijk zijn omdat de kolom LOH Survival Rate % 1% zegt.
U kunt extra ETW-gebeurtenissen verzamelen die u vertellen wie deze grote objecten heeft toegewezen. De volgende opdrachtregel:
perfview /GCOnly /AcceptEULA /nogui collect
verzamelt een AllocationTick-gebeurtenis, die ongeveer elke 100k aan toewijzingen wordt geactiveerd. Met andere woorden, een gebeurtenis wordt geactiveerd telkens wanneer een groot object wordt toegewezen. U kunt vervolgens een van de GC Heap Alloc-weergaven bekijken, waarin u de callstacks ziet die grote objecten hebben toegewezen:
Zoals u ziet, is dit een zeer eenvoudige test die alleen grote objecten toewijst vanuit de Main
methode.
Een debugger
Als alles wat u hebt een geheugendump is en u moet kijken welke objecten daadwerkelijk op de LOH staan, kunt u de sos-foutopsporingsprogramma-extensie van .NET gebruiken.
Notitie
De foutopsporingsopdrachten die in deze sectie worden genoemd, zijn van toepassing op de Windows-foutopsporingsprogramma's.
Hieronder ziet u voorbeelduitvoer van het analyseren van de 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
De heapgrootte van LOH is (16.754.224 + 16.699.288 + 16.284.504) = 49.738.016 bytes. Tussen adressen 023e1000 en 033db630 worden 8.008.736 bytes bezet door een matrix van System.Object objecten, worden 6.663.696 bytes bezet door een matrix met System.Byte objecten en 2.081.792 bytes bezet door vrije ruimte.
Soms toont het foutopsporingsprogramma aan dat de totale grootte van de LOH kleiner is dan 85.000 bytes. Dit gebeurt omdat de runtime zelf de LOH gebruikt om bepaalde objecten toe te wijzen die kleiner zijn dan een groot object.
Omdat de LOH niet gecomprimeerd is, wordt de LOH soms beschouwd als de bron van fragmentatie. Fragmentatie betekent:
Fragmentatie van de beheerde heap, die wordt aangegeven door de hoeveelheid vrije ruimte tussen beheerde objecten. In SoS geeft de
!dumpheap –type Free
opdracht de hoeveelheid vrije ruimte weer tussen beheerde objecten.Fragmentatie van de adresruimte van het virtuele geheugen (VM), het geheugen dat is gemarkeerd als
MEM_FREE
. U kunt het ophalen met behulp van verschillende foutopsporingsprogramma-opdrachten in windbg.In het volgende voorbeeld ziet u fragmentatie in de VM-ruimte:
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)
Het is gebruikelijker om VM-fragmentatie te zien die wordt veroorzaakt door tijdelijke grote objecten waarvoor de garbagecollector regelmatig nieuwe beheerde heap-segmenten van het besturingssysteem moet verkrijgen en lege segmenten terug naar het besturingssysteem moeten vrijgeven.
Als u wilt controleren of de LOH vm-fragmentatie veroorzaakt, kunt u een onderbrekingspunt instellen op VirtualAlloc en VirtualFree om te zien wie deze heeft aangeroepen. Als u bijvoorbeeld wilt zien wie probeert virtuele geheugensegmenten toe te wijzen die groter zijn dan 8 MB vanuit het besturingssysteem, kunt u een onderbrekingspunt als volgt instellen:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
Met deze opdracht wordt ingebroken in het foutopsporingsprogramma en wordt alleen de aanroepstack weergegeven als VirtualAlloc wordt aangeroepen met een toewijzingsgrootte die groter is dan 8 MB (0x800000).
CLR 2.0 heeft een functie met de naam VM Hoarding toegevoegd die nuttig kan zijn voor scenario's waarin segmenten (waaronder op de grote en kleine object-heaps) vaak worden verkregen en vrijgegeven. Als u VM Hoarding wilt opgeven, geeft u een opstartvlag op die wordt aangeroepen STARTUP_HOARD_GC_VM
via de hosting-API. In plaats van lege segmenten weer vrij te geven aan het besturingssysteem, wordt het geheugen op deze segmenten door de CLR gedecommitteerd en op een stand-bylijst geplaatst. (Houd er rekening mee dat de CLR dit niet doet voor segmenten die te groot zijn.) De CLR gebruikt deze segmenten later om te voldoen aan nieuwe segmentaanvragen. De volgende keer dat uw app een nieuw segment nodig heeft, gebruikt de CLR er een uit deze stand-bylijst als deze een segment kan vinden dat groot genoeg is.
VM-hoarding is ook handig voor toepassingen die de segmenten willen vasthouden die ze al hebben verkregen, zoals sommige server-apps die de dominante apps zijn die op het systeem worden uitgevoerd, om onvoldoende geheugenuitzondering te voorkomen.
We raden u ten zeerste aan uw toepassing zorgvuldig te testen wanneer u deze functie gebruikt om ervoor te zorgen dat uw toepassing redelijk stabiel geheugengebruik heeft.