Garbage Collector Basics and Performance Hints (Concetti di base di Garbage Collector e suggerimenti per le prestazioni)
Rico Mariani
Microsoft Corporation
Aprile 2003
Riepilogo: Il Garbage Collector .NET offre un servizio di allocazione ad alta velocità con un buon uso della memoria e senza problemi di frammentazione a lungo termine. Questo articolo illustra il funzionamento dei Garbage Collector, quindi illustra alcuni dei problemi di prestazioni che potrebbero verificarsi in un ambiente di Garbage Collection. (10 pagine stampate)
Si applica a:
Microsoft® .NET Framework
Contenuto
Introduzione
Modello semplificato
Raccolta della Garbage
Prestazioni
Finalizzazione
Conclusione
Introduzione
Per comprendere come usare correttamente il Garbage Collector e quali problemi di prestazioni potrebbero verificarsi durante l'esecuzione in un ambiente di Garbage Collection, è importante comprendere le nozioni di base sul funzionamento dei Garbage Collector e sul modo in cui tali operazioni interne influiscono sui programmi in esecuzione.
Questo articolo è suddiviso in due parti: prima di tutto verrà illustrata la natura del Garbage Collector di Common Language Runtime (CLR) in termini generali usando un modello semplificato e quindi verranno illustrate alcune implicazioni sulle prestazioni di tale struttura.
Modello semplificato
Per scopi esplicativi, considerare il modello semplificato seguente dell'heap gestito. Si noti che questo non è ciò che viene effettivamente implementato.
Figura 1. Modello semplificato dell'heap gestito
Le regole per questo modello semplificato sono le seguenti:
- Tutti gli oggetti garbage collectable vengono allocati da un intervallo contiguo di spazio indirizzi.
- L'heap è suddiviso in generazioni (più avanti) in modo che sia possibile eliminare la maggior parte della spazzatura esaminando solo una piccola frazione dell'heap.
- Gli oggetti all'interno di una generazione hanno quasi la stessa età.
- Le generazioni con numeri più elevati indicano aree dell'heap con gli oggetti meno recenti. Tali oggetti sono molto più probabilmente stabili.
- Gli oggetti meno recenti si trovano agli indirizzi più bassi, mentre i nuovi oggetti vengono creati a indirizzi crescenti. Gli indirizzi aumentano nella figura 1 precedente.
- Il puntatore di allocazione per i nuovi oggetti contrassegna il limite tra le aree di memoria usate (allocate) e inutilizzate (libere).
- Periodicamente l'heap viene compattato rimuovendo gli oggetti non attivi e scorrendo gli oggetti attivi verso l'estremità inferiore dell'heap. In questo modo viene espansa l'area inutilizzata nella parte inferiore del diagramma in cui vengono creati nuovi oggetti.
- L'ordine degli oggetti in memoria rimane l'ordine in cui sono stati creati, per una buona località.
- Non ci sono mai spazi tra gli oggetti nell'heap.
- Viene eseguito il commit solo di parte dello spazio disponibile. Quando necessario, ** maggiore memoria viene acquisita dal sistema operativo nell'intervallo di indirizzi riservati .
Raccolta della Garbage
Il tipo di raccolta più semplice da comprendere è la Garbage Collection completamente compatta, quindi inizierò a discutere di questo.
Raccolte complete
In una raccolta completa è necessario arrestare l'esecuzione del programma e trovare tutte le radici nell'heap GC. Queste radici si presentano in una varietà di forme, ma sono in particolare stack e variabili globali che puntano all'heap. A partire dalle radici, visitiamo ogni oggetto e seguiamo ogni puntatore a oggetti contenuti in ogni oggetto visitato che contrassegna gli oggetti mentre andiamo avanti. In questo modo l'agente di raccolta avrà trovato ogni oggetto raggiungibile o attivo . Gli altri oggetti, quelli irraggiungibili , sono ora condannati.
Figura 2. Radici nell'heap GC
Dopo aver identificato gli oggetti non raggiungibili, si vuole recuperare lo spazio per usarlo in un secondo momento; l'obiettivo dell'agente di raccolta a questo punto è far scorrere gli oggetti live verso l'alto ed eliminare lo spazio sprecato. Con l'esecuzione arrestata, l'agente di raccolta può spostare tutti gli oggetti e correggere tutti i puntatori in modo che tutto sia collegato correttamente nella nuova posizione. Gli oggetti sopravvissuti vengono promossi al numero di generazione successiva (ovvero i limiti per le generazioni vengono aggiornati) e l'esecuzione può riprendere.
Raccolte parziali
Sfortunatamente, la Garbage Collection completa è semplicemente troppo costosa da eseguire ogni volta, quindi ora è opportuno discutere come la presenza di generazioni nella raccolta ci aiuta.
Prima di tutto si consideri un caso immaginario in cui siamo straordinariamente fortunati. Si supponga che sia presente una raccolta completa recente e che l'heap sia ben compattato. L'esecuzione del programma riprende e si verificano alcune allocazioni. Infatti, si verificano molti e molti allocazioni e dopo un numero sufficiente di allocazioni il sistema di gestione della memoria decide che è il momento di raccogliere.
Ecco dove siamo fortunati. Si supponga che in tutto il tempo in cui eravamo in esecuzione dopo l'ultima raccolta in cui non è stato scritto alcuno degli oggetti meno recenti , solo gli oggetti appena allocati (generazione0), gli oggetti sono stati scritti. Se ciò dovesse verificarsi, ci si troverebbe in una situazione ottimale perché è possibile semplificare il processo di Garbage Collection in modo massiccio.
Invece della nostra consueta raccolta completa, possiamo solo presupporre che tutti gli oggetti meno recenti (gen1, gen2) siano ancora attivi, o almeno abbastanza di essi sono vivi che non vale la pena guardare questi oggetti. Inoltre, poiché nessuno di loro è stato scritto (ricorda quanto siamo fortunati?) non ci sono puntatori dagli oggetti meno recenti agli oggetti più recenti. Quindi quello che possiamo fare è guardare tutte le radici come al solito, e se qualsiasi radice punta a oggetti vecchi semplicemente ignorare quelli. Per altre radici (quelle che puntano alla generazione0) procediamo come di consueto, seguendo tutti i puntatori. Ogni volta che troviamo un puntatore interno che torna negli oggetti meno recenti, lo ignoriamo.
Al termine di questo processo, ogni oggetto attivo verrà visitato in gen0 senza aver visitato oggetti delle generazioni precedenti. Gli oggetti gen0 possono quindi essere condannati come di consueto e si scorre verso l'alto solo quella regione di memoria, lasciando gli oggetti meno recenti indisturbati.
Ora questa è davvero una grande situazione per noi perché sappiamo che la maggior parte dello spazio morto è probabilmente in oggetti più giovani dove c'è una grande quantità di varianza. Molte classi creano oggetti temporanei per i valori restituiti, le stringhe temporanee e altre classi di utilità diverse, ad esempio enumeratori e non. Guardando solo gen0 , è possibile tornare alla maggior parte dello spazio morto osservando solo pochi oggetti.
Sfortunatamente, non siamo mai abbastanza fortunati a usare questo approccio, perché almeno alcuni oggetti meno recenti sono associati a modifiche in modo che puntino a nuovi oggetti. Se ciò accade non è sufficiente per ignorarli.
Creazione di generazioni che funzionano con le barriere di scrittura
Per fare in modo che l'algoritmo precedente funzioni effettivamente, è necessario sapere quali oggetti meno recenti sono stati modificati. Per ricordare la posizione degli oggetti dirty, viene usata una struttura di dati denominata tabella schede e per mantenere questa struttura di dati, il compilatore di codice gestito genera le cosiddette barriere di scrittura. Queste due nozioni sono fondamentali per il successo della garbage collection basata sulla generazione.
La tabella delle schede può essere implementata in diversi modi, ma il modo più semplice per considerarlo è una matrice di bit. Ogni bit nella tabella delle schede rappresenta un intervallo di memoria nell'heap, ad esempio 128 byte. Ogni volta che un programma scrive un oggetto in un indirizzo, il codice di barriera di scrittura deve calcolare quale blocco di 128 byte è stato scritto e quindi impostare il bit corrispondente nella tabella delle schede.
Con questo meccanismo è ora possibile rivedere l'algoritmo di raccolta. Se si esegue un'operazione di Garbage Collection di generazione0 , è possibile usare l'algoritmo come illustrato in precedenza, ignorando eventuali puntatori alle generazioni precedenti, ma dopo aver eseguito questa operazione, è necessario trovare anche ogni puntatore a oggetti in ogni oggetto che si trova in un blocco contrassegnato come modificato nella tabella delle schede. Dobbiamo trattare quelle come radici. Se si considerano anche questi puntatori, si raccoglieranno correttamente solo gli oggetti gen0 .
Questo approccio non sarebbe affatto utile se la tabella delle carte fosse sempre piena, ma in pratica pochi puntatori delle generazioni più vecchie vengono effettivamente modificati, quindi vi è un notevole risparmio da questo approccio.
Prestazioni
Ora che è disponibile un modello di base per il funzionamento delle cose, si considerino alcune cose che potrebbero andare storte che lo renderebbero lento. Questo ci darà una buona idea quali tipi di cose dovremmo cercare di evitare di ottenere le migliori prestazioni dall'agente di raccolta.
Troppe allocazioni
Questa è davvero la cosa più semplice che può andare storta. L'allocazione di nuova memoria con Il Garbage Collector è molto veloce. Come si può notare nella figura 2 precedente, tutto ciò che deve accadere in genere è che il puntatore di allocazione venga spostato per creare spazio per il nuovo oggetto sul lato "allocato", non si ottiene molto più veloce di quello. Tuttavia, prima o poi un Garbage Collection deve accadere e, tutte le cose sono uguali, è meglio che accada più tardi che prima. Quindi si vuole assicurarsi che quando si creano nuovi oggetti che siano realmente necessari e appropriati per farlo, anche se la creazione di un solo oggetto è veloce.
Questo può sembrare un consiglio ovvio, ma in realtà è estremamente facile dimenticare che una piccola riga di codice che si scrive potrebbe attivare un sacco di allocazioni. Si supponga, ad esempio, di scrivere una funzione di confronto di un tipo e si supponga che gli oggetti dispongano di un campo di parole chiave e che si voglia che il confronto sia senza distinzione tra maiuscole e minuscole sulle parole chiave nell'ordine specificato. In questo caso non è possibile confrontare l'intera stringa di parole chiave, perché la prima parola chiave potrebbe essere molto breve. Sarebbe allettante usare String.Split per suddividere la stringa della parola chiave in parti e quindi confrontare ogni pezzo in ordine usando il confronto normale senza distinzione tra maiuscole e minuscole. Sembra fantastico, giusto?
Beh, come si scopre fare come questo non è una buona idea. Si noterà che String.Split creerà una matrice di stringhe, il che significa un nuovo oggetto stringa per ogni parola chiave originariamente nella stringa delle parole chiave più un altro oggetto per la matrice. Yikes! Se si esegue questa operazione nel contesto di un ordinamento, questo è un sacco di confronti e la funzione di confronto a due righe crea ora un numero molto elevato di oggetti temporanei. Improvvisamente il Garbage Collector sta andando a lavorare molto duramente per vostro conto, e anche con lo schema di raccolta più intelligente c'è solo un sacco di cestino per pulire. Meglio scrivere una funzione di confronto che non richiede affatto le allocazioni.
allocazioni Too-Large
Quando si lavora con un allocatore tradizionale, ad esempio malloc(), i programmatori scrivono spesso codice che effettua il minor numero possibile di chiamate a malloc() perché sanno che il costo dell'allocazione è relativamente elevato. Ciò si traduce nella pratica dell'allocazione in blocchi, spesso di allocazione speculativa di oggetti necessari, in modo da poter eseguire un minor numero di allocazioni totali. Gli oggetti pre-allocati vengono quindi gestiti manualmente da un certo tipo di pool, creando in modo efficace una sorta di allocatore personalizzato ad alta velocità.
Nel mondo gestito questa pratica è molto meno convincente per diversi motivi:
In primo luogo, il costo di eseguire un'allocazione è estremamente basso: non c'è alcuna ricerca di blocchi gratuiti come con gli allocatori tradizionali; tutto ciò che deve accadere è il limite tra le aree libere e allocate deve spostarsi. Il costo basso dell'allocazione significa che il motivo più interessante per il pool non è semplicemente presente.
In secondo luogo, se si sceglie di pre-allocare, naturalmente si eseguiranno più allocazioni di quelle necessarie per le esigenze immediate, che a sua volta potrebbero forzare operazioni di Garbage Collection aggiuntive che altrimenti non erano necessarie.
Infine, il Garbage Collector non sarà in grado di recuperare spazio per gli oggetti che si stanno riciclando manualmente, perché dal punto di vista globale tutti gli oggetti, inclusi quelli che non sono attualmente in uso, sono ancora attivi. Si potrebbe notare che una grande quantità di memoria è sprecata mantenendo oggetti pronti all'uso, ma non in uso a portata di mano.
Questo non è dire che l'allocazione preliminare è sempre una cattiva idea. Si potrebbe voler eseguire questa operazione per forzare l'allocazione iniziale di determinati oggetti, ad esempio, ma è probabile che sia meno interessante come strategia generale rispetto al codice non gestito.
Troppi puntatori
Se si crea una struttura di dati costituita da una grande mesh di puntatori, si avranno due problemi. Prima di tutto, ci saranno molte scritture di oggetti (vedere la figura 3 seguente) e, in secondo luogo, quando si tratta di raccogliere tale struttura di dati, si farà in modo che il Garbage Collector segua tutti i puntatori e, se necessario, modificarli tutti man mano che si spostano. Se la struttura dei dati è di lunga durata e non cambierà molto, l'agente di raccolta dovrà solo visitare tutti i puntatori quando si verificano raccolte complete (al livello gen2 ). Tuttavia, se si crea una struttura di questo tipo su base transitoria, ad esempio nell'ambito delle transazioni di elaborazione, si pagherà il costo molto più spesso.
Figura 3. Struttura dei dati pesante nei puntatori
Anche le strutture di dati pesanti nei puntatori possono avere altri problemi, non correlati al tempo di Garbage Collection. Anche in questo caso, come illustrato in precedenza, quando gli oggetti vengono allocati in modo contiguo nell'ordine di allocazione. Questa operazione è ottimale se si crea una struttura di dati di grandi dimensioni, possibilmente complessa, ad esempio ripristinando le informazioni da un file. Anche se si dispone di tipi di dati diversi, tutti gli oggetti saranno vicini in memoria, che a sua volta consentiranno al processore di avere accesso rapido a tali oggetti. Tuttavia, man mano che passa il tempo e la struttura dei dati viene modificata, è probabile che i nuovi oggetti debbano essere associati agli oggetti precedenti. Questi nuovi oggetti saranno stati creati molto più tardi e quindi non saranno vicini agli oggetti originali in memoria. Anche quando il Garbage Collector compatta la memoria, gli oggetti non verranno casuali in memoria, semplicemente "scivolano" insieme per rimuovere lo spazio sprecato. Il disordine risultante potrebbe diventare così male nel tempo che si potrebbe essere propensi a creare una nuova copia dell'intera struttura di dati, tutto ben compresso, e lasciare che il vecchio disordine uno venga condannato dal collezionista in due corso.
Troppe radici
Il Garbage Collector deve naturalmente dare alle radici un trattamento speciale al momento della raccolta, che devono sempre essere enumerati e considerati correttamente a loro volta. La raccolta di generazione0 può essere veloce solo nella misura in cui non si dà un'inondazione di radici da considerare. Se si desidera creare una funzione ricorsiva profonda con molti puntatori a oggetti tra le variabili locali, il risultato può effettivamente essere piuttosto costoso. Questo costo è dovuto non solo alla necessità di considerare tutte queste radici, ma anche nel numero extra-elevato di oggetti di generazione0 che tali radici potrebbero essere mantenute attive per non molto tempo (discusso di seguito).
Troppe scritture di oggetti
Ancora una volta si fa riferimento alla discussione precedente, tenere presente che ogni volta che viene attivato anche un programma gestito che ha modificato un puntatore a un oggetto, viene attivato anche il codice della barriera di scrittura. Questo può essere negativo per due motivi:
In primo luogo, il costo della barriera di scrittura potrebbe essere paragonabile al costo di ciò che si stava cercando di fare in primo luogo. Se ad esempio si esegue operazioni semplici in una classe di enumeratore, è possibile che sia necessario spostare alcuni puntatori chiave dalla raccolta principale all'enumeratore in ogni passaggio. Questo è in realtà un elemento che si potrebbe voler evitare, perché si raddoppia in modo efficace il costo di copia di tali puntatori a causa della barriera di scrittura e potrebbe essere necessario farlo una o più volte per ciclo sull'enumeratore.
In secondo luogo, l'attivazione di barriere di scrittura è grave se si sta scrivendo effettivamente su oggetti meno recenti. Quando si modificano gli oggetti meno recenti, si creano in modo efficace radici aggiuntive per controllare (descritte in precedenza) quando si verifica la successiva operazione di Garbage Collection. Se hai modificato abbastanza dei tuoi vecchi oggetti, nega effettivamente i normali miglioramenti della velocità associati alla raccolta solo della generazione più giovane.
Questi due motivi sono naturalmente integrati dai soliti motivi per non fare troppe scritture in qualsiasi tipo di programma. Tutte le cose sono uguali, è meglio toccare meno della memoria (lettura o scrittura, in realtà) in modo da rendere più economico l'uso della cache del processore.
Troppi oggetti a vita quasi lunga
Infine, forse la più grande insidie del Garbage Collector generazionale è la creazione di molti oggetti, che non sono esattamente temporanei né sono esattamente di lunga durata. Questi oggetti possono causare un sacco di problemi, perché non verranno puliti da una raccolta di generazione0 (il più economico), perché saranno ancora necessari, e potrebbero anche sopravvivere a una raccolta di generazione1 perché sono ancora in uso, ma presto muoiono dopo questo.
Il problema è che, una volta che un oggetto è arrivato al livello gen2 , solo una raccolta completa ne verrà liberata e le raccolte complete sono sufficientemente costose che il Garbage Collector li ritarda purché sia ragionevolmente possibile. Quindi il risultato di avere molti oggetti di "durata quasi lunga" è che la tua generazione2 tenderà a crescere, potenzialmente a una velocità allarmante; potrebbe non essere pulito quasi il più velocemente possibile, e quando viene pulito sarà certamente molto più costoso farlo di quanto si potrebbe desiderare.
Per evitare questi tipi di oggetti, le migliori linee di difesa sono simili alle seguenti:
- Allocare il minor numero possibile di oggetti, con la dovuta attenzione alla quantità di spazio temporaneo in uso.
- Mantenere al minimo le dimensioni degli oggetti di durata più lunga.
- Mantenere il minor numero possibile di puntatori a oggetti nello stack (si tratta di radici).
Se si eseguono queste operazioni, è più probabile che le raccolte di generazione0 siano molto efficaci e la generazione1 non crescerà molto velocemente. Di conseguenza, le raccolte di generazione1 possono essere eseguite meno frequentemente e, quando diventa prudente eseguire una raccolta di generazione1 , gli oggetti a durata media saranno già morti e possono essere recuperati, a buon mercato, in quel momento.
Se le cose vanno benissimo, durante le operazioni a stato stabile le dimensioni della generazione2 non saranno affatto in aumento!
Finalizzazione
Ora che sono stati trattati alcuni argomenti con il modello di allocazione semplificata, vorrei complicare leggermente le cose in modo da poter discutere un fenomeno più importante, e questo è il costo dei finalizzatori e della finalizzazione. Brevemente, un finalizzatore può essere presente in qualsiasi classe, ovvero un membro facoltativo che il Garbage Collector promette di chiamare su oggetti altrimenti non recapitabili prima di recuperare la memoria per tale oggetto. In C# si usa la sintassi ~Class per specificare il finalizzatore.
Impatto della finalizzazione sulla raccolta
Quando il Garbage Collector rileva per la prima volta un oggetto altrimenti inattivo, ma deve comunque essere finalizzato, deve abbandonare il tentativo di recuperare lo spazio per tale oggetto in quel momento. L'oggetto viene invece aggiunto a un elenco di oggetti che richiedono la finalizzazione e, inoltre, l'agente di raccolta deve assicurarsi che tutti i puntatori all'interno dell'oggetto rimangano validi fino al completamento della finalizzazione. Questa è fondamentalmente la stessa cosa di dire che ogni oggetto in bisogno di finalizzazione è come un oggetto radice temporaneo dal punto di vista dell'agente di raccolta.
Al termine della raccolta, il thread di finalizzazione denominato aptly passerà attraverso l'elenco degli oggetti che richiedono la finalizzazione e richiamerà i finalizzatori. Quando questo viene fatto, gli oggetti diventano nuovamente morti e verranno naturalmente raccolti nel modo normale.
Finalizzazione e prestazioni
Con questa conoscenza di base della finalizzazione è già possibile dedurre alcune cose molto importanti:
Prima di tutto, gli oggetti che richiedono la finalizzazione sono più lunghi degli oggetti che non lo fanno. Infatti, possono vivere molto più a lungo. Si supponga, ad esempio, chesia necessario finalizzare un oggetto di seconda generazione. La finalizzazione verrà pianificata, ma l'oggetto è ancora in generazione2, quindi non verrà raccolto nuovamente fino a quando non si verifica la raccolta di generazione2 successiva. Questo potrebbe essere un tempo molto lungo e, in effetti, se le cose stanno andando bene sarà molto tempo, perché le raccolte di generazione2 sono costose e quindi vogliamo che accadano molto raramente. Gli oggetti meno recenti che richiedono la finalizzazione potrebbero dover attendere decine se non centinaia di raccolte di generazione0 prima che venga recuperato lo spazio.
In secondo luogo, gli oggetti che necessitano di finalizzazione causano danni collaterali. Poiché i puntatori a oggetti interni devono rimanere validi, non solo gli oggetti che richiedono direttamente la finalizzazione rimangono in memoria, ma tutto ciò che l'oggetto fa riferimento, direttamente e indirettamente, rimarrà anche in memoria. Se un albero enorme di oggetti è stato ancorato da un singolo oggetto che richiedeva la finalizzazione, l'intero albero persisterebbe, potenzialmente per molto tempo come appena discusso. È quindi importante usare i finalizzatori con moderazione e posizionarli su oggetti con il minor numero possibile di puntatori a oggetti interni. Nell'esempio di albero appena dato, è possibile evitare facilmente il problema spostando le risorse in bisogno di finalizzazione in un oggetto separato e mantenendo un riferimento a tale oggetto nella radice dell'albero. Con questa modesta modifica solo l'unico oggetto (si spera un bel oggetto piccolo) potrebbe rimanere persistente e il costo di finalizzazione è ridotto al minimo.
Infine, gli oggetti che richiedono la creazione della finalizzazione creano il lavoro per il thread finalizzatore. Se il processo di finalizzazione è un processo complesso, l'unico e solo il thread finalizzatore spenderà molto tempo per l'esecuzione di tali passaggi, che può causare un backlog di lavoro e quindi causare un ritardo di più oggetti in attesa della finalizzazione. Pertanto, è fondamentale che i finalizzatori eseseguono il minor lavoro possibile. Tenere presente anche che anche se tutti i puntatori a oggetti rimangono validi durante la finalizzazione, è possibile che tali puntatori portino a oggetti che sono già stati finalizzati e potrebbero quindi essere meno utili. In genere è più sicuro evitare di seguire i puntatori a oggetti nel codice di finalizzazione anche se i puntatori sono validi. Un percorso di codice di finalizzazione sicuro e breve è il migliore.
IDisposable e Dispose
In molti casi è possibile che gli oggetti che altrimenti debbano essere sempre finalizzati per evitare tale costo implementando l'interfaccia IDisposable . Questa interfaccia fornisce un metodo alternativo per recuperare le risorse la cui durata è ben nota al programmatore e che in realtà accade abbastanza. Naturalmente è meglio ancora se gli oggetti usano solo memoria e quindi non richiedono alcuna finalizzazione o eliminazione; ma se la finalizzazione è necessaria e ci sono molti casi in cui la gestione esplicita degli oggetti è semplice e pratica, l'implementazione dell'interfaccia IDisposable è un ottimo modo per evitare, o almeno ridurre, i costi di finalizzazione.
Nel linguaggio C# questo modello può essere molto utile:
class X: IDisposable
{
public X(…)
{
… initialize resources …
}
~X()
{
… release resources …
}
public void Dispose()
{
// this is the same as calling ~X()
Finalize();
// no need to finalize later
System.GC.SuppressFinalize(this);
}
};
Quando una chiamata manuale a Dispose elimina la necessità dell'agente di raccolta di mantenere attivo l'oggetto e chiamare il finalizzatore.
Conclusione
Il Garbage Collector di .NET offre un servizio di allocazione ad alta velocità con un buon uso della memoria e nessun problema di frammentazione a lungo termine, tuttavia è possibile eseguire operazioni che offrono prestazioni molto inferiori a quelle ottimali.
Per ottenere il massimo dall'allocatore, è consigliabile prendere in considerazione procedure come le seguenti:
- Allocare tutta la memoria (o il più possibile) da usare contemporaneamente con una determinata struttura di dati.
- Rimuovere le allocazioni temporanee che possono essere evitate con una piccola penalità in complessità.
- Ridurre al minimo il numero di volte in cui i puntatori a oggetti vengono scritti, in particolare le scritture eseguite in oggetti meno recenti.
- Ridurre la densità dei puntatori nelle strutture di dati.
- Usare in modo limitato i finalizzatori e quindi solo sugli oggetti "foglia", il più possibile. Interrompere gli oggetti, se necessario, per facilitare questa operazione.
Una procedura regolare per esaminare le strutture dei dati chiave e condurre profili di utilizzo della memoria con strumenti come Allocation Profiler consente di mantenere efficace l'utilizzo della memoria e di avere il Garbage Collector che funziona al meglio.