Scusate il ritardo....direbbe qualcuno. Un altro comunque direbbe...meglio tardi che mai. Comunque sia, vediamo finalmente di affrontare l'argomento del garbage collection per il Compact Framework (CF), che tanto interesse ha suscitato durante la Mobility Conference di Milano, tanto da indurmi a pensare che intorno a questo "topic" c'è ancora un pò di...mistero. Allora il concetto chiave è che sia il CF che il Full framework .NET utilzzano entrambi la tecnica del Garbage Collector(GC). Esistono però delle sostanziali differenze nel modo in cui entrambe operano. Non sto qui ad analizzare nel dettaglio l'operatività del GC per la versione Full, ma mi basti ricordare che opera sulla base del concetto delle "generazioni".
GC nel Full .NET Framework
In pratica il nostro heap gestito viene suddiviso in 3 parti (e solo 3) chiamate generazioni. La prima generazione (la G0) è quella dove vengono allocati tutti i nuovi oggetti. Così facendo sarà la prima a saturarsi, ma in questo modo si permette al GC di concentrarsi su una relativamente piccola porzione di memoria dove troverà sicuramente la maggior parte degli oggetti non più referenziati ( questo sulla base del principio che più un oggetto è stato creato da poco, più è probabile che sarà abbandonato presto). Non appena si cerca di allocare un nuovo oggetto nella G0 ormai piena, viene sollevata un eccezione, il GC va in esecuzione, trova tutti gli oggetti non più referenziati in G0, sposta gli oggetti ancora in vita nella parte dello heap definita Generazione 1 (aggiustando quindi i puntatori), svuota la G0 degli oggetti non più referenziati e torna a dormire sino alla prossima eccezione. E' ovvio che arriverà un momento in cui anche la G1 si saturerà insieme alla G0, per cui in quel momento la collezione degli oggetti avverrà sulle due porzioni di Heap (G0 e G1) portando quelli ancora in vita in G2, svuotando la G0 e portando gli eventuali oggetti ancora vivi in G1. Se voi come me, utilizzate pesantemente i tool di Sysinternals, con Process Explorer potrete vedere i dettagli delle generazioni. Infatti per ciascun processo che carichi al suo interno il Common Language Runtime (CLR), evidenziato in giallo nella lista dei processi, potrete vedere qualcosa di molto simile a quello mostrato in figura:
Diciamo subito che la situazione sopra riprodotta è dovuta ad un'applcazione windows form con una textbox ed un bottone, chegira su un laptop con 1 G di Ram ed in condizioni di affollamento medio alto della RAM, Qusto per dire che il Runtime di .NET decide ad ogni "collection" di modificare dinamicamente le dimensioni delle 3 generazioni per motivi di performance.
GC nel Compact Framework
Detto questo, vediamo invece qual'è la filosofia che sta alla base del GC nella versione CF. Innanzitutto non ci sono le generazioni. Comprensibile visto che lo heap riservato agli oggetti del framework è molto più limitato che nelle macchine desktop. Quindi, prima domanda, quando entra in azione il GC? Il GC scatta al presentarsi di diverse occasioni:
- Si raggiunge un limite di 750KB di oggetti allocati
- L'applicazione perde il suo focus e viene posta in background
- Non c'è più spazio nello heap
- invocazione diretta tramite GC.Collect();
Quando scatta l'allarme a causa di una delle condizioni sopra esposte il GC esegue una raccolta, o come spesso viene riportato in letteratura, una collezione. Esistono 3 tipi di collezioni, cioè 3 modi di operare per raccogliere gli oggetti non più referenziati:
Simple Collection . E' l’algoritmo più semplice che si basa sul concetto di Marca e Getta (Mark & Sweep). La memoria che non contiene riferimenti attivi viene marcata come non attiva e deallocata. Ovviamente questo metodo è molto veloce ma ha un inconveniente molto evidente: alla lunga causerà una frammentazione della memoria stessa.
Compact Collection. Ad un certo punto la memoria avrà bisogno di essere ricompattata (non appena arrivata ad una certa soglia di framentazione). E’ il momento allora della Compact Collection. Verrà mandata in esecuzione una simple collection e di seguito i frammenti relativi agli oggetti ancora attivi, vengono spostati un un’area di memoria contigua. Ricorda molto lo spostamento degli oggetti nelle generazioni superiori nel Full .NET Framework. In questo modo saranno disponibili quindi chunk di heap di dimensioni più grandi per successive allocazioni.
Full collection. Si esegue quando c’è bisogno di una grande quantità di memoria. Viene eseguita una Compact Collection (vedi sopra) e di seguito tutti i metodi JITTed (cioè tutto quel codice che da MSIL è stato trasformato in codice nativo) vengono rilasciati o, come ritroviamo nella documentazione tecnica, “Pitched”. E’ il caso in cui un’applicazione PPC viene mandata in background e al ripristino il codice, all’occorrenza, viene nuovamente ricompilato dal JIT.
L'algoritmo è estremamente efficiente, ma non pensiamo di utilizzarlo a nostro piacimento invocando la GC.Collect() per migliorare l'occupazione della memoria, Questo comportamento infatti è altamente sconsigliato e foriero di notevoli degradi di performance. Il motivo si trova nel meccanismo del GC stesso. Infatti, prima che questo vada in esecuzione, il CLR cerca di portare tutti i thread in uno stato sicuro, o come si legge spesso, portare i threads in un "safe point". Uno stato in cui ciascun thread non potrà modificare lo Heap gestito. Per far questo si portano i thread a non eseguire nessuna riga di codice gestito o anche codice nativo del CLR sino a che il GC non abbia terminato il suo lavoro. Per questo motivo far partire una collect significa tutte le volte porre i vari threads in una sorta di stato di "sospensione" che impedirebbe all'applicazione una costante e regolare esecuzione del codice con conseguente sicuro degrado delle performance.
Vi rimando inoltre ad un mio post in cui vengono illustrate le tecniche per indagare le performance di un'applicazione basata sul CF e che riportano i tipi di collezioni intraprese dal GC