Principi fondamentali di Garbage Collection

In Common Language Runtime (CLR), il Garbage Collector (GC) funge da gestore di memoria automatico. Il Garbage Collector gestisce l'allocazione e il release di memoria per un'applicazione. Pertanto, gli sviluppatori che lavorano con codice gestito non hanno bisogno di scrivere codice per eseguire attività di gestione della memoria. La gestione automatica della memoria consente di evitare che si verifichino problemi frequenti legati alla gestione della memoria, quali la mancata liberazione di un oggetto e una conseguente perdita di memoria, o il tentativo di accesso alla memoria liberata per un oggetto già liberato.

Questo articolo illustra i concetti principali di Garbage Collection.

Vantaggi

Il Garbage Collector offre i seguenti vantaggi:

  • Libera gli sviluppatori dalla necessità di rilasciare manualmente la memoria.

  • Alloca gli oggetti nell'heap gestito in maniera efficiente.

  • Recupera gli oggetti inutilizzati, ne cancella la memoria e tiene la memoria a disposizione per le future allocazioni. Gli oggetti gestiti ottengono automaticamente contenuto pulito con il quale iniziare, in modo che i costruttori non debbano inizializzare ogni campo dati.

  • Garantisce la sicurezza della memoria, assicurandosi che un oggetto non possa usare la memoria allocata per un altro oggetto.

Nozioni fondamentali sulla memoria

Nel seguente elenco sono riepilogati i concetti fondamentali relativi alla memoria CLR:

  • Ogni processo dispone di un proprio spazio degli indirizzi virtuali distinto. Tutti i processi nello stesso computer condividono la stessa memoria fisica e il file di paging, se presente.

  • Per impostazione predefinita, nei computer a 32 bit ogni processo dispone di uno spazio degli indirizzi virtuali in modalità utente da 2 GB.

  • Uno sviluppatore di applicazioni usa solo lo spazio degli indirizzi virtuali e non modifica mai direttamente la memoria fisica. Il Garbage Collector alloca e libera automaticamente la memoria virtuale nell'heap gestito.

    Se si sta scrivendo in codice nativo, si usano funzioni Windows per lavorare con lo spazio indirizzi virtuale. Queste funzioni allocano e liberano automaticamente la memoria virtuale negli heap nativi.

  • La memoria virtuale può trovarsi in tre stati:

    Stato Descrizione
    Libero Non vi sono riferimenti al blocco di memoria, che è disponibile per l'allocazione.
    Prenotato Il blocco di memoria è disponibile per l'utilizzo e non può essere usato per altre richieste di allocazione. Non è tuttavia possibile archiviare dati in questo blocco di memoria fino a quando non ne verrà eseguito il commit.
    Commit eseguito Il blocco di memoria è assegnato all'archiviazione fisica.
  • Lo spazio indirizzi virtuale potrebbe essere frammentato, ovvero, potrebbero essere presenti blocchi liberi, chiamati hole, nello spazio degli indirizzi. Quando viene richiesta un'allocazione della memoria virtuale, il gestore di memoria virtuale deve trovare un singolo blocco libero di dimensioni sufficienti a soddisfare la richiesta di allocazione. Anche se 2 GB di spazio sono disponibili, un'allocazione che richiede 2 GB avrà esito negativo a meno che tutto lo spazio disponibile non si trovi in un unico blocco di indirizzi.

  • Si potrebbe esaurire la memoria se lo spazio degli indirizzi virtuali non è sufficiente a riservare o lo spazio fisico non è sufficiente a eseguire il commit.

    Il file di paging viene usato anche se la pressione della memoria fisica (richiesta di memoria fisica) è bassa. La prima volta che la pressione della memoria fisica è elevata, il sistema operativo deve fare spazio nella memoria fisica per archiviare dati ed esegue il backup di alcuni dei dati che si trovano nella memoria fisica nel file di paging. Il paging dei dati non viene eseguito fino a quando non è necessario; pertanto, è possibile riscontrare il paging in situazioni in cui la pressione della memoria fisica è bassa.

Allocazione della memoria

Quando si inizializza un nuovo processo, per tale processo viene riservata una regione contigua di spazio degli indirizzi. Lo spazio degli indirizzi riservato viene definito heap gestito. Nell'heap gestito viene conservato un puntatore all'indirizzo in cui verrà allocato il successivo oggetto dell'heap. Le impostazioni iniziali del puntatore corrispondono all'indirizzo di base dell'heap gestito. Tutti i tipi di riferimento vengono allocati nell'heap gestito. Quando il primo tipo di riferimento viene creato da un'applicazione, per tale tipo viene allocata memoria nell'indirizzo di base dell'heap gestito. Quando l'oggetto successivo viene creato dall'applicazione, la memoria destinata a tale oggetto viene allocata dal runtime nello spazio degli indirizzi immediatamente successivo al primo oggetto. Lo spazio per i nuovi oggetti verrà allocato in questo modo dal runtime fino all'esaurimento dello spazio degli indirizzi.

L'allocazione della memoria dall'heap gestito risulta più veloce dell'allocazione di memoria non gestita. Poiché la memoria per un oggetto viene allocata da Common Language Runtime aggiungendo un valore a un puntatore, tale operazione risulta veloce quasi quanto l'allocazione di memoria dallo stack. Inoltre, poiché i nuovi oggetti allocati consecutivamente vengono archiviati contiguamente nell'heap gestito, un’applicazione può accedervi rapidamente.

Release di memoria

Il modulo di ottimizzazione del Garbage Collector consente di determinare il momento migliore per l'esecuzione di una raccolta sulla base delle allocazioni in corso. Durante l'esecuzione di una raccolta, la memoria per gli oggetti non più utilizzati dall'applicazione viene rilasciata dal Garbage Collector. Individua gli oggetti non più in uso esaminando le radici dell’applicazione. Le radici di un'applicazione includono campi statici, variabili locali nello stack di un thread, registri della CPU, handle GC e coda di finalizzazione. Ogni radice fa riferimento a un oggetto dell'heap gestito o è impostata su null. Il Garbage Collector può richiede queste radici al resto del runtime. Il Garbage Collector usa questo elenco per creare un grafo contenente tutti gli oggetti raggiungibili dalle radici.

Gli oggetti non inclusi nel grafo non sono raggiungibili dalle radici dell'applicazione. Il Garbage Collector considera gli oggetti non raggiungibili come da eliminare e rilascia la memoria per essi allocata. Nel corso di una raccolta, l'heap gestito viene esaminato dal Garbage Collector, alla ricerca dei blocchi di spazi degli indirizzi occupati da oggetti non raggiungibili. Quando un oggetto non raggiungibile viene rilevato, viene utilizzata una funzione di copia della memoria che consente di ricompattare lo spazio allocato per gli oggetti ancora raggiungibili nella memoria, liberando i blocchi di spazi degli indirizzi allocati per oggetti non raggiungibili. Una volta compattata la memoria per gli oggetti non raggiungibili, il Garbage Collector aggiorna i puntatori agli oggetti ai rispettivi nuovi indirizzi, in modo che le radici dell'applicazione puntino agli oggetti nelle rispettive nuove posizioni. Il puntatore relativo all'heap gestito viene inoltre posizionato dopo l'ultimo oggetto non raggiungibile.

La compressione della memoria viene effettuata solo se durante la raccolta viene rilevato un numero significativo di oggetti non raggiungibili. Se tutti gli oggetti dell'heap gestito sopravvivono a una raccolta, non è necessaria alcuna compressione della memoria.

Per migliorare le prestazioni, la memoria per oggetti di grandi dimensioni viene allocata da Common Language Runtime in un heap separato. La memoria per oggetti di grandi dimensioni viene rilasciata automaticamente dal Garbage Collector. Tuttavia, per evitare lo spostamento di oggetti di grandi dimensioni nella memoria, la compattazione della memoria non viene solitamente effettuata.

Condizioni per un'operazione di Garbage Collection

Le operazioni di Garbage Collection vengono eseguite in presenza di una delle seguenti condizioni:

  • La memoria fisica del sistema è insufficiente. Le dimensioni della memoria vengono rilevate grazie alla notifica di memoria insufficiente del sistema operativo o di memoria insufficiente indicata dall'host.

  • La memoria usata dagli oggetti allocati nell'heap gestito supera una soglia accettabile. Questa soglia viene continuamente modificata durante l'esecuzione del processo.

  • Viene chiamato il metodo GC.Collect . Nella quasi totalità dei casi non è necessario ricorrere a questo metodo, poiché il Garbage Collector funziona senza interruzioni. Il metodo viene usato principalmente in situazioni eccezionali e per scopi di test.

Heap gestito

Dopo che CLR inizializza il Garbage Collector, quest’ultimo alloca un segmento di memoria per archiviare e gestire oggetti. Questa memoria è definita heap gestito, in contrapposizione a un heap nativo presente nel sistema operativo.

Per ogni processo gestito esiste un heap gestito. Tutti i thread nel processo allocano memoria per gli oggetti sullo stesso heap.

Per riservare memoria, il Garbage Collector chiama la funzione di Windows VirtualAlloc e riserva un segmento di memoria alla volta per le applicazioni gestite. Il Garbage Collector riserva anche segmenti secondo le esigenze e li rilascia al sistema operativo dopo aver cancellato tutti gli oggetti, chiamando la funzione di Windows VirtualFree.

Importante

La dimensione dei segmenti allocati dal Garbage Collector è specifica dell'implementazione ed è soggetta a modifiche in qualsiasi momento, tra cui aggiornamenti periodici. L'applicazione non deve dare per scontata o dipendere da una particolare dimensione del segmento, né provare a configurare la quantità di memoria disponibile per le allocazioni di segmenti.

Minore è il numero di oggetti allocati nell'heap, minore sarà il lavoro del Garbage Collector. Quando si allocano oggetti, non usare valori arrotondati per eccesso che superino le proprie esigenze; ad esempio, non allocare una matrice di 32 byte se si necessita solo di 15 byte.

Quando viene attivata un'operazione di Garbage Collection, il Garbage Collector recupera la memoria occupata da oggetti inutilizzati. Durante il processo di recupero, gli oggetti attivi vengono compattati in modo da poter essere spostati insieme e lo spazio inutilizzato viene rimosso, riducendo le dimensioni dell'heap. Questo processo garantisce che gli oggetti allocati insieme restino uniti nell'heap gestito, preservandone la località.

L'impatto (frequenza e durata) delle operazioni di Garbage Collection è il risultato del volume di allocazioni e della quantità di memoria esclusa nell'heap gestito.

L'heap può essere considerato l'insieme di due heap: l'heap degli oggetti grandi e l'heap degli oggetti piccoli. L'heap di oggetti di grandi dimensioni contiene oggetti di 85.000 byte o di dimensioni maggiori, che consistono in genere in matrici. Gli oggetti di istanza sono raramente di grandi dimensioni.

Suggerimento

È possibile configurare le dimensioni di soglia per oggetti da inserire nell'heap di oggetti di grandi dimensioni.

Generazioni

L'algoritmo GC si basa su varie considerazioni:

  • La compattazione della memoria per una parte dell'heap gestito risulta più rapida della compressione per l'intero heap gestito.
  • Gli oggetti più recenti hanno durate più brevi, quelli meno recenti durate più lunghe.
  • Gli oggetti più recenti sono solitamente correlati e l'applicazione vi accede circa nello stesso momento.

L’operazione di Garbage Collection recupera principalmente oggetti di breve durata. Per ottimizzare le prestazioni del Garbage Collector, l'heap gestito è suddiviso in tre generazioni, 0, 1 e 2, così da poter gestire separatamente oggetti di lunga e breve durata. Il Garbage Collector archivia i nuovi oggetti nella generazione 0. Gli oggetti creati nelle prime fasi della durata dell'applicazione che non vengono raccolti vengono promossi e archiviati nelle generazioni 1 e 2. Poiché la compressione di una porzione dell'heap gestito risulta più rapida della compressione dell'intero heap, questo schema consente al Garbage Collector di rilasciare la memoria in una specifica generazione anziché per l'intero heap gestito a ogni raccolta.

  • Generazione 0: questa generazione è la più giovane e contiene oggetti di breve durata. Un esempio di oggetto di breve durata è una variabile temporanea. Le operazioni di Garbage Collection vengono eseguite il più delle volte in questa generazione.

    Gli oggetti appena allocati formano una nuova generazione e diventano implicitamente raccolte di generazione 0. Tuttavia, se si tratta di oggetti di grandi dimensioni, questi passano all'heap di oggetti di grandi dimensioni (LOH), talvolta definito generazione 3. La generazione 3 è una generazione fisica logicamente raccolta come parte della generazione 2.

    La maggior parte degli oggetti vengono recuperati tramite Garbage Collection nella generazione 0 e non passano alla generazione successiva.

    Se un'applicazione tenta di creare un nuovo oggetto quando la generazione 0 è piena, il Garbage Collector esegue una raccolta per liberare spazio indirizzi per l'oggetto. Il Garbage Collector esamina prima di tutto gli oggetti presenti nella generazione 0, anziché tutti gli oggetti presenti nell'heap gestito. L'effettuazione di una raccolta sulla sola generazione 0 spesso consente di recuperare memoria sufficiente a consentire all'applicazione di continuare a creare nuovi oggetti.

  • Generazione 1: questa generazione contiene oggetti di breve durata e funge da buffer tra gli oggetti di breve durata e quelli di lunga durata.

    Dopo aver eseguito una raccolta della generazione 0, il Garbage Collector compatta la memoria per gli oggetti raggiungibili e li promuove alla generazione 1. Poiché la durata degli oggetti non raccolti è solitamente più lunga, la promozione a una generazione superiore risulta opportuna. Il riesame degli oggetti nelle generazioni 1 e 2 da parte del Garbage Collector non sarà necessario a ogni raccolta della generazione 0.

    Se una raccolta di generazione 0 non recupera memoria sufficiente perché l'applicazione possa creare un nuovo oggetto, Il Garbage Collector può eseguire una raccolta di generazione 1 e quindi di generazione 2. Gli oggetti presenti nella generazione 1 non raccolti vengono promossi alla generazione 2.

  • Generazione 2: questa generazione contiene oggetti di lunga durata. Un esempio di oggetto di lunga durata è un oggetto in un'applicazione server contenente dati statici che restano attivi per la durata del processo.

    Gli oggetti nella generazione 2 che sopravvivono a una raccolta rimangono nella generazione 2 fino a quando non sono determinati come non raggiungibili in una raccolta futura.

    Anche gli oggetti nell'heap di oggetti di grandi dimensioni (talvolta definito generazione 3) vengono raccolti nella generazione 2.

Le operazioni di Garbage Collection vengono eseguite in generazioni specifiche a seconda delle condizioni. Raccogliere una generazione significa raccogliere gli oggetti in quella generazione e in tutte le generazioni più recenti. Un'operazione di Garbage Collection di generazione 2 viene definita completa, in quanto recupera gli oggetti in tutte le generazioni (vale a dire, tutti gli oggetti nell'heap gestito).

Esclusione e promozioni

Gli oggetti che non vengono recuperati durante un'operazione di Garbage Collection sono definiti oggetti sopravvissuti e vengono promossi alla generazione successiva:

  • Gli oggetti che sopravvivono a un'operazione di Garbage Collection della generazione 0 vengono promossi alla generazione 1.
  • Gli oggetti che sopravvivono a un'operazione di Garbage Collection della generazione 1 vengono promossi alla generazione 2.
  • Gli oggetti che sopravvivono a un'operazione di Garbage Collection della generazione 2 rimangono nella generazione 2.

Quando il Garbage Collector rileva un tasso elevato di sopravvivenza in una generazione, aumenta la relativa soglia delle allocazioni. La raccolta successiva ottiene una dimensione sostanziale di memoria recuperata. CLR bilancia costantemente due priorità: impedire che il working set di un'applicazione diventi troppo grande ritardando l'operazione di Garbage Collection e limitare la frequenza delle operazioni di Garbage Collection.

Generazioni e segmenti temporanei

Poiché gli oggetti nelle generazioni 0 e 1 sono di breve durata, queste sono definite generazioni temporanee.

Le generazioni temporanee sono allocate nel segmento di memoria noto come segmento temporaneo. Ogni nuovo segmento acquisito dal Garbage Collector diventa il nuovo segmento temporaneo e contiene gli oggetti esclusi da un'operazione di Garbage Collection di generazione 0. Il segmento temporaneo precedente diventa il nuovo segmento di generazione 2.

La dimensione del segmento temporaneo varia a seconda del sistema, a 32 bit o a 64 bit, e al tipo di procedura di Garbage Collector in esecuzione (workstation o server GC). La seguente tabella mostra le dimensioni predefinite del segmento temporaneo:

Workstation/server GC 32 bit 64 bit
GC workstation 16 MB 256 MB
GC server 64 MB 4 GB
Server GC con > 4 CPU logiche 32 MB 2 GB
Server GC con > 8 CPU logiche 16 MB 1 GB

Il segmento temporaneo può includere oggetti di generazione 2, Gli oggetti di generazione 2 possono usare più segmenti, nella misura richiesta dal processo e consentita dalla memoria.

La quantità di memoria liberata da un'operazione di Garbage Collection temporanea è limitata alla dimensione del segmento temporaneo. La quantità di memoria liberata è proporzionale allo spazio occupato dagli oggetti inutilizzati.

Fasi di un'operazione di Garbage Collection

Un'operazione di Garbage Collection si compone delle seguenti fasi:

  • Una fase di contrassegno in cui vengono individuati tutti gli oggetti attivi e ne viene creato un elenco.

  • Una fase di rilocazione in cui vengono aggiornati i riferimenti agli oggetti che saranno compattati.

  • Una fase di compattazione in cui lo spazio occupato dagli oggetti inutilizzati viene recuperato e gli oggetti esclusi compattati. Durante la fase di compressione, gli oggetti rimasti dopo un'operazione di Garbage Collection vengono spostati verso l'estremità meno recente del segmento.

    Poiché le raccolte di generazione 2 possono occupare più segmenti, gli oggetti promossi alla generazione 2 possono essere spostati in un segmento meno recente. Gli oggetti sopravvissuti di generazione 1 e 2 possono essere spostati in un segmento diverso, in quanto vengono promossi alla generazione 2.

    In genere, l'heap oggetti di grandi dimensioni (LOH) non viene compresso perché la copia di oggetti grandi impone un calo delle prestazioni. Tuttavia, in .NET Core e in .NET Framework 4.5.1 e versioni successive è possibile usare la proprietà GCSettings.LargeObjectHeapCompactionMode per comprimere l'heap di oggetti di grandi dimensioni su richiesta. Inoltre, il LOH è automaticamente compresso quando viene impostato un limite massimo specificando una delle due seguenti opzioni:

Per stabilire se gli oggetti sono attivi, il Garbage Collector usa le seguenti informazioni:

  • radici stack: variabili dello stack fornite dal compilatore JIT (Just-In-Time) e dallo stack walker. Le ottimizzazioni JIT possono allungare o abbreviare le aree di codice in cui le variabili dello stack vengono segnalate al Garbage Collector.

  • Handle di Garbage Collection : handle che puntano a oggetti gestiti e che possono essere allocati mediante codice utente o Common Language Runtime.

  • Dati statici: oggetti statici nei domini applicazione che potrebbero fare riferimento ad altri oggetti. Ogni dominio applicazione tiene traccia dei rispettivi oggetti statici.

Prima di eseguire un'operazione di Garbage Collection, tutti i thread gestiti vengono sospesi, eccetto il thread che attiva l'operazione.

Nella seguente illustrazione è mostrato un thread che attiva un'operazione di Garbage Collection causando la sospensione degli altri thread:

Screenshot of how a thread triggers a Garbage Collection.

Risorse non gestite

Le attività di gestione della memoria necessarie vengono effettuate automaticamente dal Garbage Collector per la maggior parte degli oggetti creati dall'applicazione. Per le risorse non gestite è tuttavia necessario il rilascio esplicito. Il tipo più comune di risorsa non gestita è rappresentato da un oggetto che esegue il wrapping di una risorsa del sistema operativo, quale un handle di file, un handle di finestra o una connessione di rete. Benché il Garbage Collector sia in grado di monitorare la durata di un oggetto gestito in cui è incapsulata una risorsa non gestita, non dispone di dati sufficienti per effettuare il rilascio della risorsa.

Quando si definisce un oggetto che incapsula una risorsa non gestita, è consigliabile specificare il codice necessario per pulire la risorsa non gestita in un metodo Dispose pubblico. Specificando un metodo Dispose, si consente agli utenti dell'oggetto di rilasciare esplicitamente la risorsa dopo l'uso dell'oggetto. Quando si usa un oggetto che incapsula una risorsa non gestita, assicurarsi di chiamare Dispose quando necessario.

È anche necessario fornire un metodo di rilascio delle risorse non gestite nel caso in cui un consumer del tipo in uso dimentichi di chiamare Dispose. È possibile usare un handle sicuro per eseguire il wrapping della risorsa non gestita oppure eseguire l'override del metodo Object.Finalize().

Per ulteriori informazioni sulla pulizia delle risorse non gestite, consultare Pulire le risorse non gestite.

Vedi anche