Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Il Garbage Collector (GC) di .NET divide gli oggetti in oggetti di piccole e grandi dimensioni. Quando un oggetto è grande, alcuni dei relativi attributi diventano più significativi rispetto a se l'oggetto è piccolo. Ad esempio, compattarlo, ovvero copiarlo in memoria altrove nell'heap, può essere costoso. Per questo motivo, il Garbage Collector inserisce oggetti di grandi dimensioni nell'heap di oggetti di grandi dimensioni (LOH). Questo articolo illustra cosa qualifica un oggetto come oggetto di grandi dimensioni, come vengono raccolti oggetti di grandi dimensioni e quali implicazioni per le prestazioni impongono oggetti di grandi dimensioni.
Importante
Questo articolo illustra l'heap di oggetti di grandi dimensioni in .NET Framework e .NET Core in esecuzione solo nei sistemi Windows. Non copre il LOH in esecuzione su implementazioni .NET su altre piattaforme.
Come un oggetto finisce sulla LOH
Se un oggetto è maggiore o uguale a 85.000 byte di dimensioni, viene considerato un oggetto di grandi dimensioni. Questo numero è stato determinato dall'ottimizzazione delle prestazioni. Quando una richiesta di allocazione di oggetti è per 85.000 o più byte, il runtime lo alloca nell'heap di oggetti di grandi dimensioni.
Per comprendere cosa significa, è utile esaminare alcuni concetti fondamentali sul Garbage Collector.
Il Garbage Collector è un collettore generazionale. Ha tre generazioni: generazione 0, generazione 1 e generazione 2. Il motivo della presenza di tre generazioni è che, in un'app ben ottimizzata, la maggior parte degli oggetti muore in gen0. Ad esempio, in un'app server, le allocazioni associate a ogni richiesta devono morire al termine della richiesta. Le richieste di allocazione in corso entreranno in gen1 e termineranno lì. Essenzialmente, gen1 funge da buffer tra aree di oggetti giovani e aree di oggetti di lunga durata.
Gli oggetti appena allocati formano una nuova generazione di oggetti e sono raccolte di generazione 0 implicite. Tuttavia, se sono oggetti di grandi dimensioni, passano all'heap di oggetti di grandi dimensioni (LOH), che a volte viene definito generazione 3. La generazione 3 è una generazione fisica che viene raccolta logicamente come parte della generazione 2.
Gli oggetti di grandi dimensioni appartengono alla generazione 2 perché vengono raccolti solo durante una raccolta di seconda generazione. Quando viene raccolta una generazione, vengono raccolte anche tutte le generazioni più giovani. Ad esempio, quando si verifica un GC di prima generazione, vengono raccolte sia la generazione 0 che la generazione 1. Quando si verifica un GC di seconda generazione, l'intero heap viene raccolto. Per questo motivo, un GC di generazione 2 viene chiamato anche GC completo. Questo articolo si riferisce alla generazione 2 GC invece di GC completo, ma i termini sono intercambiabili.
Le generazioni offrono una visualizzazione logica dell'heap GC. Fisicamente, gli oggetti vivono in segmenti di heap gestiti. Un segmento heap gestito è un blocco di memoria che il GC riserva dal sistema operativo chiamando la funzione VirtualAlloc per conto del codice gestito. Quando viene caricato il CLR, il processo GC alloca due segmenti di heap iniziali: uno per oggetti di piccole dimensioni (l'heap di oggetti piccoli, o SOH) e uno per gli oggetti di grandi dimensioni (l'heap di oggetti di grandi dimensioni).
Le richieste di allocazione vengono quindi soddisfatte inserendo gli oggetti gestiti in questi segmenti di heap gestiti. Se l'oggetto è minore di 85.000 byte, viene inserito nel segmento per SOH; in caso contrario, viene inserito nel segmento LOH. Viene eseguito il commit dei segmenti (in blocchi più piccoli) perché più oggetti vengono allocati su di essi. Per l'oggetto SOH, gli oggetti che sopravvivono a un GC vengono promossi alla generazione successiva. Gli oggetti che sopravvivono a una raccolta di generazione 0 sono ora considerati oggetti di generazione 1 e così via. Tuttavia, gli oggetti che sopravvivono alla generazione meno recente sono ancora considerati nella generazione meno recente. In altre parole, i sopravvissuti della generazione 2 sono oggetti di seconda generazione; e i sopravvissuti dalla LOH sono oggetti LOH (che vengono raccolti con gen2).
Il codice utente può essere allocato solo nella generazione 0 (oggetti di piccole dimensioni) o nella loH (oggetti di grandi dimensioni). Solo il GC può "allocare" oggetti nella generazione 1 (promuovendo i sopravvissuti dalla generazione 0) e la generazione 2 (promuovendo i sopravvissuti dalla generazione 1).
Quando viene attivata una Garbage Collection, il GC traccia gli oggetti attivi, e li compatta. Ma poiché la compattazione è costosa, il GC spazza il LOH; crea una lista libera di oggetti inutilizzati che possono essere riutilizzati in un secondo momento per soddisfare le richieste di allocazione di oggetti di grandi dimensioni. Gli oggetti morti adiacenti vengono combinati in un unico oggetto libero.
.NET Core e .NET Framework (a partire da .NET Framework 4.5.1) includono la GCSettings.LargeObjectHeapCompactionMode proprietà che consente agli utenti di specificare che loH deve essere compattato durante il successivo GC di blocco completo. In futuro, .NET potrebbe decidere di compattare automaticamente l'loH. Ciò significa che, se si allocano oggetti di grandi dimensioni e si vuole assicurarsi che non vengano spostati, dovresti comunque fissarli.
La figura 1 illustra uno scenario in cui il GC forma la generazione 1 dopo la prima raccolta di generazione 0, dove Obj1 e Obj3 sono morti, e forma la generazione 2 dopo la prima raccolta di generazione 1 in cui Obj2 e Obj5 sono morti. Si noti che questa e le figure seguenti sono solo a scopo illustrativo; contengono pochissimi oggetti per mostrare meglio cosa accade nell'heap. In realtà, molti altri oggetti sono in genere coinvolti in un GC.
Figura 1: GC di generazione 0 e generazione 1.
Nella figura 2 viene illustrato che, dopo un GC di seconda generazione che ha rilevato che Obj1 e Obj2 sono morti, il GC forma uno spazio libero contiguo nella memoria che era precedentemente occupato da Obj1 e Obj2, il quale è stato quindi utilizzato per soddisfare una richiesta di allocazione per Obj4. Lo spazio dopo l'ultimo oggetto, Obj3, alla fine del segmento può essere usato anche per soddisfare le richieste di allocazione.
Figura 2: Dopo un GC di seconda generazione
Se lo spazio disponibile non è sufficiente per soddisfare le richieste di allocazione di oggetti di grandi dimensioni, il processo GC tenta innanzitutto di acquisire più segmenti dal sistema operativo. In caso di errore, attiva un GC di seconda generazione nella speranza di liberare spazio.
Durante un processo di Garbage Collection di generazione 1 o 2, il garbage collector rilascia segmenti che non contengono oggetti attivi, restituendoli al sistema operativo tramite la funzione VirtualFree. Lo spazio fino alla fine del segmento dopo l'ultimo oggetto attivo viene decommesso (ad eccezione del segmento temporaneo in cui si trova gen0/gen1, dove il Garbage Collector mantiene una parte dello spazio impegnata perché la tua applicazione effettuerà subito delle allocazioni). Gli spazi liberi rimangono sottoposti a commit anche se vengono reimpostati, il che significa che il sistema operativo non deve scrivere i dati su disco.
Poiché il LOH viene raccolto solo durante una generazione 2 di GC, il segmento LOH può essere liberato solo durante tale GC. La figura 3 illustra uno scenario in cui il Garbage Collector rilascia un segmento (segmento 2) al sistema operativo e rimuove più spazio nei segmenti rimanenti. Se deve usare lo spazio decommesso alla fine del segmento per soddisfare richieste di allocazione di oggetti di grandi dimensioni, esegue di nuovo il commit della memoria. Per una spiegazione del commit/decommit, vedere la documentazione per VirtualAlloc.
Figura 3: LOH dopo un GC di seconda generazione
Quando viene raccolto un oggetto di grandi dimensioni?
In generale, un GC si verifica in una delle tre condizioni seguenti:
L'allocazione supera la soglia della generazione 0 o quella degli oggetti di grandi dimensioni.
La soglia è una proprietà di una generazione. Una soglia per una generazione viene impostata quando il Garbage Collector alloca gli oggetti al suo interno. Quando viene superata la soglia, viene attivato un GC in tale generazione. Quando si allocano oggetti di piccole o grandi dimensioni, si consumano rispettivamente le soglie della generazione 0 e del LOH. Quando il Garbage Collector alloca nella generazione 1 e 2, consuma le loro soglie. Queste soglie vengono ottimizzate dinamicamente durante l'esecuzione del programma.
Questo è il caso tipico; la maggior parte dei GC si verifica a causa di allocazioni nell'heap gestito.
Viene chiamato il metodo GC.Collect .
Se il metodo senza GC.Collect() parametri viene chiamato o viene passato GC.MaxGeneration un altro overload come argomento, l'heap gestito viene raccolto insieme al resto dell'heap gestito.
Il sistema è in una situazione di memoria insufficiente.
Ciò si verifica quando il Garbage Collector riceve una notifica di memoria elevata dal sistema operativo. Se il garbage collector ritiene che una raccolta GC di seconda generazione sarà utile, ne attiva una.
Implicazioni sulle prestazioni LOH
Le allocazioni sull'heap di oggetti di grandi dimensioni influiscono sulle prestazioni nei modi seguenti.
Costo di allocazione.
CLR garantisce che la memoria per ogni nuovo oggetto che restituisce viene cancellata. Ciò significa che il costo di allocazione di un oggetto di grandi dimensioni è dominato dalla cancellazione della memoria (a meno che non attivi un GC). Se sono necessari due cicli per cancellare un byte, sono necessari 170.000 cicli per cancellare il più piccolo oggetto grande. La cancellazione della memoria di un oggetto da 16 MB in un computer a 2 GHz richiede circa 16 ms. È un costo piuttosto elevato.
Costo raccolta.
Poiché l'elemento LOH e la generazione 2 vengono raccolti insieme, se viene superata una delle soglie, viene attivata una raccolta di seconda generazione. Se viene attivata una raccolta di seconda generazione a causa del LOH, la generazione 2 non sarà necessariamente molto più piccola dopo la raccolta GC. Se non sono presenti molti dati sulla generazione 2, questo ha un impatto minimo. Tuttavia, se la generazione 2 è di grandi dimensioni, può causare problemi di prestazioni se vengono attivati molti GC di seconda generazione. Se molti oggetti di grandi dimensioni vengono allocati su base temporanea e si dispone di un SOH di grandi dimensioni, si potrebbe impiegare troppo tempo a fare garbage collection. Inoltre, il costo di allocazione può davvero sommarsi se si continua ad allocare e rilasciare oggetti molto grandi.
Elementi della matrice con tipi di riferimento.
Gli oggetti estremamente grandi nel LOH sono in genere array (è molto raro avere un oggetto di istanza così grande). Se gli elementi di una matrice sono ricchi di riferimenti, comporta un costo che non è presente se gli elementi non sono ricchi di riferimenti. Se l'elemento non contiene riferimenti, non è necessario che il Garbage Collector attraversi affatto la matrice. Ad esempio, se si usa una matrice per archiviare i nodi in un albero binario, un modo per implementarlo consiste nel fare riferimento al nodo destro e sinistro di un nodo in base ai nodi effettivi:
class Node { Data d; Node left; Node right; }; Node[] binary_tr = new Node [num_nodes];Se
num_nodesè grande, il Garbage Collector deve eseguire almeno due riferimenti per elemento. Un approccio alternativo consiste nell'archiviare l'indice dei nodi destro e sinistro:class Node { Data d; uint left_index; uint right_index; } ;Invece di fare riferimento ai dati del nodo sinistro come
left.d, si fa riferimento a esso comebinary_tr[left_index].d. E il Garbage Collector non deve esaminare i riferimenti per il nodo sinistro e destro.
Tra i tre fattori, i primi due sono in genere più significativi del terzo. Per questo motivo, è consigliabile allocare un pool di oggetti di grandi dimensioni riutilizzati anziché allocare quelli temporanei.
Raccogliere i dati sulle prestazioni per il LOH
Prima di raccogliere i dati sulle prestazioni per un'area specifica, è necessario aver già eseguito le operazioni seguenti:
- Sono state trovate prove che dovresti esaminare questa area.
- Hai esaurito altre aree che conosci senza trovare nulla che potrebbe spiegare il problema di prestazione riscontrato.
Per altre informazioni sui concetti fondamentali della memoria e della CPU, vedere il blog Informazioni sul problema prima di provare a trovare una soluzione.
È possibile usare gli strumenti seguenti per raccogliere dati sulle prestazioni LOH:
Contatori delle prestazioni della memoria .NET CLR
I contatori delle prestazioni di memoria CLR .NET sono in genere un buon primo passaggio nell'analisi dei problemi di prestazioni (anche se è consigliabile usare eventi ETW). Un modo comune per esaminare i contatori delle prestazioni consiste nell'usare Performance Monitor (perfmon.exe). Selezionare Aggiungi (CTRL + A) per aggiungere i contatori interessanti per i processi di cui si è a cuore. È possibile salvare i dati del contatore delle prestazioni in un file di log.
I due contatori seguenti nella categoria Memoria CLR .NET sono rilevanti per lo standard LOH:
Raccolte di generazione 2
Visualizza il numero di volte in cui si sono verificati i garbage collection di seconda generazione dall'inizio del processo. Il contatore viene incrementato alla fine di una raccolta di generazione 2 , detta anche Garbage Collection completa. Questo contatore visualizza l'ultimo valore osservato.
Dimensione dell'heap di oggetti grandi
Visualizza le dimensioni correnti, in byte, incluso lo spazio libero, dell'LOH. Questo contatore viene aggiornato alla fine di un'operazione di Garbage Collection, non a ogni allocazione.
È anche possibile eseguire query sui contatori delle prestazioni a livello di codice usando la PerformanceCounter classe . Per il LOH, specificare ".NET CLR Memory" come CategoryName e "Large Object Heap size" come CounterName.
PerformanceCounter performanceCounter = new()
{
CategoryName = ".NET CLR Memory",
CounterName = "Large Object Heap size",
InstanceName = "<instance_name>"
};
Console.WriteLine(performanceCounter.NextValue());
È comune raccogliere programmaticamente i contatori come parte di un processo di test di routine. Quando si individuano contatori con valori non comuni, usare altri mezzi per ottenere dati più dettagliati per facilitare l'indagine.
Annotazioni
È consigliabile usare gli eventi ETW anziché i contatori delle prestazioni, perché ETW fornisce informazioni molto più complete.
Eventi ETW
Il Garbage Collector offre un set completo di eventi ETW che aiutano a comprendere le attività dell'heap e le loro motivazioni. I post di blog seguenti illustrano come raccogliere e comprendere gli eventi GC con ETW:
Per identificare un numero eccessivo di GC di generazione 2 causato da allocazioni LOH temporanee, esaminare la colonna Trigger Reason per i controller di dominio. Per un semplice test che alloca solo oggetti di grandi dimensioni temporanei, è possibile raccogliere informazioni sugli eventi ETW con il comando PerfView seguente:
perfview /GCCollectOnly /AcceptEULA /nogui collect
Il risultato è simile al seguente:
Come si può notare, tutti i GCS sono di seconda generazione e sono tutti attivati da AllocLarge, il che significa che l'allocazione di un oggetto di grandi dimensioni ha attivato questo GC. Sappiamo che queste allocazioni sono temporanee perché il tasso di sopravvivenza LOH % colonna indica 1%.
È possibile raccogliere eventi ETW aggiuntivi che indicano chi ha allocato questi oggetti di grandi dimensioni. La seguente riga di comando:
perfview /GCOnly /AcceptEULA /nogui collect
raccoglie un evento AllocationTick, che scatta circa ogni 100.000 allocazioni. In altre parole, viene generato un evento ogni volta che viene allocato un oggetto di grandi dimensioni. Puoi quindi esaminare una delle viste Heap Alloc del GC, che mostrano gli stack di chiamate che hanno allocato oggetti di grandi dimensioni:
Come si può notare, si tratta di un test molto semplice che alloca solo oggetti di grandi dimensioni dal relativo Main metodo.
Un debugger
Se si dispone di un dump della memoria ed è necessario esaminare gli oggetti effettivamente presenti nel file LOH, è possibile usare l'estensione del debugger SoS fornita da .NET.
Annotazioni
I comandi di debug indicati in questa sezione sono applicabili ai debugger di Windows.
Di seguito è riportato l'output di esempio dell'analisi del file 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
La dimensione dell'heap LOH è (16.754.224 + 16.699.288 + 16.284.504) = 49.738.016 byte. Tra gli indirizzi 023e1000 e 033db630, 8.008.736 byte sono occupati da una matrice di System.Object oggetti, 6.663.696 byte sono occupati da una matrice di System.Byte oggetti e 2.081.792 byte sono occupati da spazio libero.
In alcuni casi, il debugger mostra che le dimensioni totali del loH sono inferiori a 85.000 byte. Ciò si verifica perché il runtime stesso usa lo loH per allocare alcuni oggetti più piccoli di un oggetto di grandi dimensioni.
Poiché la LOH non è compattata, a volte il LOH è considerato la fonte di frammentazione. Frammentazione significa:
Frammentazione dell'heap gestito, indicata dalla quantità di spazio disponibile tra gli oggetti gestiti. In SoS il
!dumpheap –type Freecomando visualizza la quantità di spazio disponibile tra gli oggetti gestiti.Frammentazione dello spazio indirizzi della memoria virtuale (VM), ovvero la memoria contrassegnata come
MEM_FREE. È possibile ottenerlo usando vari comandi del debugger in windbg.L'esempio seguente mostra la frammentazione nello spazio della macchina virtuale:
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)
È più comune vedere la frammentazione delle macchine virtuali causata da oggetti temporanei di grandi dimensioni che richiedono al Garbage Collector di acquisire frequentemente nuovi segmenti di heap gestiti dal sistema operativo e rilasciare quelli vuoti nel sistema operativo.
Per verificare se il LOH sta causando la frammentazione della VM, puoi impostare un punto di interruzione su VirtualAlloc e VirtualFree per scoprire chi li ha invocati. Ad esempio, per vedere chi ha tentato di allocare blocchi di memoria virtuale di dimensioni superiori a 8 MB dal sistema operativo, è possibile impostare un punto di interruzione simile al seguente:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
Questo comando entra nel debugger e mostra la pila delle chiamate solo se VirtualAlloc viene chiamato con una dimensione di allocazione maggiore di 8 MB (0x800000).
CLR 2.0 ha aggiunto una funzionalità denominata VM Hoarding che può essere utile per scenari in cui i segmenti (inclusi gli heap di oggetti grandi e piccoli) vengono spesso acquisiti e rilasciati. Per specificare Vm Hoarding, è necessario specificare un flag di avvio denominato STARTUP_HOARD_GC_VM tramite l'API di hosting. Invece di rilasciare segmenti vuoti nel sistema operativo, CLR decommette la memoria in questi segmenti e li inserisce in un elenco di standby. Si noti che CLR non esegue questa operazione per i segmenti troppo grandi. CLR usa successivamente tali segmenti per soddisfare le nuove richieste di segmento. La prossima volta che la tua app ha bisogno di un nuovo segmento, il CLR usa uno da questa lista di standby se può trovarne uno abbastanza grande.
L'accumulo di macchine virtuali è utile anche per le applicazioni che desiderano mantenere i segmenti già acquisiti, come alcune app server che sono predominanti nel sistema, per evitare eccezioni di memoria insufficiente.
È consigliabile testare attentamente l'applicazione quando si usa questa funzionalità per assicurarsi che l'applicazione abbia un utilizzo di memoria abbastanza stabile.