Gestione automatica della memoria
La gestione automatica della memoria è uno dei servizi forniti da Common Language Runtime durante l'esecuzione gestita. L'allocazione e il rilascio di memoria per un'applicazione vengono gestiti dal Garbage Collector di Common Language Runtime. Agli sviluppatori non viene quindi richiesta la scrittura di codice per eseguire attività di gestione della memoria quando si sviluppano applicazioni gestite. La gestione automatica della memoria consente di evitare che si verifichino i problemi consueti legati alla gestione della memoria, quale la mancata liberazione di un oggetto e il conseguente spreco di memoria allocata ma non più referenziabile o il tentativo di accesso alla memoria per un oggetto già liberato. In questa sezione viene descritta la modalità utilizzata dal Garbage Collector per l'allocazione e il rilascio di memoria.
Allocazione di 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 Garbage Collector nello spazio degli indirizzi immediatamente successivo al primo oggetto. Lo spazio per i nuovi oggetti verrà allocato in questo modo dal Garbage Collector 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 tramite un incremento di un valore a un puntatore, tale operazione risulta veloce almeno quanto l'allocazione di memoria dallo stack. Poiché inoltre i nuovi oggetti allocati consecutivamente vengono archiviati in modo contiguo nell'heap gestito, l'accesso a tali oggetti da parte dell'applicazione risulta molto rapido.
Rilascio 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. L'individuazione degli oggetti non più in uso viene effettuata tramite l'esame delle radici dell'applicazione. Ogni applicazione dispone di un insieme di radici. Ogni radice fa riferimento a un oggetto dell'heap gestito o è impostata su null. Nelle radici di un'applicazione sono inclusi puntatori a oggetti globali e statici, variabili locali e parametri relativi all'oggetto Reference nello stack di un thread e infine i registri della CPU. Al Garbage Collector è consentito l'accesso all'elenco delle radici attive mantenute dal compilatore JIT e dal runtime. L'utilizzo dell'elenco consente al Garbage Collector l'esame delle radici dell'applicazione e la creazione, nel corso di tale esame, di un grafico contenente tutti gli oggetti raggiungibili dalle directory radice.
Gli oggetti non inclusi nel grafo non sono raggiungibili dalle radici dell'applicazione. Gli oggetti non raggiungibili vengono considerati dal Garbage Collector come oggetti da eliminare e la memoria allocata per tali oggetti viene rilasciata. 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. Si noti che 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 superano la 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. Per evitare lo spostamento di oggetti di grandi dimensioni nella memoria, non viene effettuata la compattazione della memoria.
Generazioni e prestazioni
Per ottimizzare le prestazioni del Garbage Collector, l'heap gestito è diviso in tre generazioni: 0, 1 e 2. L'algoritmo del Garbage Collection del runtime si basa su svariate generalizzazioni la cui validità è stata verificata dai produttori di software per computer tramite sperimentazioni con gli schemi di Garbage Collection. Prima di tutto è stato rilevato che la compattazione per una parte dell'heap gestito risulta più rapida della compressione per l'intero heap gestito. In secondo luogo, la durata degli oggetti più recenti sarà inferiore alla durata degli oggetti meno recenti. Gli oggetti più recenti infine sono solitamente correlati e l'applicazione accede a tali oggetti quasi nello stesso momento.
Il Garbage Collector del runtime 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. Il processo di promozione dell'oggetto viene descritto più avanti in questo argomento. 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é rilasciare la memoria per l'intero heap gestito a ogni raccolta.
La raccolta viene in realtà effettuata dal Garbage Collector quando la generazione 0 è piena. Se un'applicazione tenta di creare un nuovo oggetto quando la generazione 0 è piena, il Garbage Collector rileva che nella generazione 0 non è più disponibile spazio degli indirizzi da allocare per l'oggetto. Per tentare di liberare spazio degli indirizzi per l'oggetto nella generazione 0, viene effettuata una raccolta. Il Garbage Collector esamina prima di tutto gli oggetti presenti nella generazione 0, anziché tutti gli oggetti presenti nell'heap gestito. Questo è infatti l'approccio più efficiente, poiché la durata degli oggetti recenti è solitamente ridotta e si presume che molti degli oggetti presenti nella generazione 0 non siano più utilizzati dall'applicazione quando si effettua una raccolta. L'effettuazione della raccolta sulla sola generazione 0 consente inoltre di recuperare spesso memoria sufficiente per consentire all'applicazione di continuare a creare nuovi oggetti.
Al termine della raccolta nella generazione 0 effettuata dal Garbage Collector, la memoria per gli oggetti raggiungibili viene compressa come spiegato in Rilascio di memoria in precedenza in questo argomento. Il Garbage Collector promuove quindi questi oggetti e questa parte dell'heap gestito viene considerata come 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à quindi necessario a ogni raccolta della generazione 0.
Una volta completata la prima raccolta della generazione 0 e una volta promossi gli oggetti raggiungibili alla generazione 1, la parte restante dell'heap gestito verrà considerata dal Garbage Collector come generazione 0. Il Garbage Collector continuerà quindi ad allocare memoria per i nuovi oggetti nella generazione 0 fino a quando la generazione 0 non risulterà piena e non sarà necessario eseguire un'altra raccolta. A questo punto il modulo di ottimizzazione del Garbage Collector consentirà di determinare se sia necessario esaminare gli oggetti delle generazioni meno recenti. Se ad esempio una raccolta effettuata nella generazione 0 non consente di recuperare memoria sufficiente per il corretto completamento del tentativo di creazione di un nuovo oggetto da parte dell'applicazione, il Garbage Collector potrà eseguire una raccolta della generazione 1, quindi della generazione 2. Se la memoria recuperata non risulta sufficiente, il Garbage Collector potrà eseguire una raccolta nelle generazioni 2, 1 e 0. Al termine di ogni raccolta, gli oggetti raggiungibili nella generazione 0 vengono compressi dal Garbage Collector e promossi alla generazione 1. Gli oggetti presenti nella generazione 1 non raccolti vengono promossi alla generazione 2. Poiché il Garbage Collector supporta solo tre generazioni, gli oggetti presenti nella generazione 2 non raccolti rimangono nella generazione 2 fino a quando non vengono considerati non raggiungibili da raccolte successive.
Rilascio di memoria per le risorse non gestite
Le attività di gestione della memoria necessarie vengono effettuate 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 tenere traccia della 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 crea un oggetto che incapsula una risorsa non gestita, si consiglia di fornire il codice necessario per il rilascio della risorsa non gestita in un metodo Dispose pubblico. Fornendo un metodo Dispose si consente agli utenti dell'oggetto di liberarne esplicitamente la memoria al termine delle operazioni che richiedono tale oggetto. Quando si utilizza un oggetto che incapsula una risorsa non gestita, si consiglia di tenere in considerazione il metodo Dispose e di chiamarlo in caso di necessità. Per ulteriori informazioni sulla pulitura di una risorsa non gestita e un esempio di modello di progettazione per l'implementazione di Dispose, vedere Garbage Collection.