I linguaggi di programmazione come il C++ comportano una gestione manuale della memoria, che può essere anche abbastanza veloce, ma altri linguaggi come il C# e il Java possono contare su una importante funzione: il garbage collector, letteralmente “netturbino”.
Si tratta di un sistema di gestione automatico della memoria, che permette di migliorare la qualità del lavoro degli sviluppatori, perché velocizza la gestione dell’allocazione e del rilascio dei dati memorizzati da parte di un’applicazione software.
Ciò implica che uno sviluppatore non dovrà preoccuparsi di scrivere un codice per eseguire attività di gestione della memoria e potrà contare su questo “netturbino” informatico che eliminerà i problemi più comuni, consentendo di allocare gli oggetti nell’heap in modo più efficiente e di recuperare gli oggetti inutilizzati, liberando memoria per le future allocazioni.
Non di meno, garantisce protezione per la memoria, assicurandosi che un oggetto non possa usare il contenuto di un altro oggetto. I vantaggi offerti dal garbage collector sono evidenti, ma per poterlo utilizzare al meglio è necessario conoscere il suo funzionamento.
Stack vs Heap: le differenze
Prima di procedere con la definizione del garbage collector e delle sue funzioni, è necessario comprendere i meccanismi di archiviazione degli oggetti nella memoria.
Nel caso di una gestione manuale della memoria, si dovrà procedere ad allocare un dato in una certa porzione di memoria e poi, una volta che quel dato non serve più, dovremo anche ricordarci di svuotare la posizione occupata, altrimenti la memoria libera si esaurisce e il programma non potrà più funzionare.
Nel caso di programmi con gestione automatica, invece, l’allocazione e svuotamento della memoria avviene attraverso due parti: lo stack e l’heap, cioè posizioni specifiche dove vengono allocati e deallocati oggetti. Anche se la memoria fisica per entrambe le parti è la stessa, queste funzionano diversamente: lo stack è utilizzato per le variabili di tipo valore che hanno una dimensione in byte fissa e garantisce un processo di allocazione veloce. Ogni volta che una variabile locale viene creata, questa sarà automaticamente allocata nello stack, per poi essere rimossa non appena esce dall’ambito, ovvero nel momento in cui non è più necessaria all’esecuzione del software.
L’heap invece è utilizzato per le variabili di tipo riferimento, che solitamente sono oggetti di grandi dimensioni, come ad esempio elenchi e vettori. Tali oggetti potrebbero essere troppo grandi per essere archiviati nello stack, oppure richiedono di rimanere archiviati anche quando le funzioni escono dall’ambito, e per questo motivo si tratta di un processo più lento.
Questo significa che ogni volta che si crea un nuovo oggetto con un linguaggio di programmazione, si sta eseguendo un’allocazione di heap, e molto probabilmente anche un’allocazione di stack, perché si creerà anche un riferimento a una variabile locale che poi punterà alla memoria effettiva del nuovo oggetto.
Garbage collector: cos’è e come funziona
La memoria di un computer non è quindi così semplice da tenere pulita, ma è un’operazione che va fatta per mantenere un basso consumo di RAM e ottenere migliori prestazioni. Se le variabili locali vengono eliminate dallo stack appena dopo l’ambito, quelle nell’heap potrebbero rimanere e accumularsi fino a intasare la memoria.
La scelta sarà quindi obbligata tra eliminare gli oggetti che non servono più e liberare memoria manualmente, con un maggior rischio di bug ed exploit di memoria. Qui entra in gioco il garbage collector, che viene eseguito in background, quindi durante il normale funzionamento del sistema, e garantisce la gestione automatica della memoria.
Il sistema analizza l’heap e lo stack dell’applicazione periodicamente e quando trova oggetti che non hanno più riferimenti, li classifica come spazzatura e li rimuove in modo sicuro senza influire sul programma.
Il sistema è raffinato e in grado di risolvere anche problemi complessi come le dipendenze circolari: dati due oggetti che fanno riferimento l’uno all’altro e quindi entrambi in possesso di un riferimento, ma senza un puntatore che li indica nel programma, e quindi non effettivamente utilizzati dal software in esecuzione, verranno comunque classificati come spazzatura ed eliminati dalla memoria.
Garbage collector: vantaggi e svantaggi dell’utilizzo
Il garbage collector agisce quindi come un “netturbino informatico” che ripulisce la memoria del sistema in uso, classificando le variabili “spazzatura” da quelle ancora necessarie e rimuovendole. Si tratta di un sistema molto utile, anzi fondamentale per un programmatore: basti pensare che i linguaggi C e C++ sono considerati difficili proprio per via del fatto che la gestione della memoria avviene solo manualmente.
Nel caso di linguaggi C# o Java, invece, gli sviluppatori potranno contare sull’aiuto del garbage collector ogni volta che ne hanno bisogno. Ad esempio, quando il sistema ha poca memoria: il garbage collector definisce in automatico una soglia oltre il quale la memoria dell’heap è considerata troppo piena e quando viene superata una percentuale definita, si attiva per ripulirla e svuotarla almeno in parte. In altri casi, potrà venire attivato manualmente, ma la pulizia non sarà senza conseguenze per il funzionamento dell’applicazione.
Attivare il garbage collector significa, infatti, generare un impatto sulle prestazioni del software e rallentarlo: il sistema di pulizia della memoria per funzionare deve momentaneamente sospendere l’esecuzione del programma, provocando inevitabilmente dei rallentamenti. Si immagini la CPU del proprio sistema: nel caso del linguaggio C++, la CPU lavorerà solo sul codice, anche quando la memoria viene ripulita, eseguendo un processo alla volta.
Quando invece si attiva il garbage collector, la CPU si concentra sul programma che continua a essere eseguito fino a quando non è stata prodotta “spazzatura” sufficiente a superare la soglia percentuale di memoria dell’heap fissata. Poi il programma viene messo in pausa, e la CPU viene impegnata per eseguire l’analisi e la raccolta dei rifiuti e infine si riavvia il programma.
Solitamente questa operazione è molto veloce ma se questa interruzione si verifica spesso, si otterrà un sensibile rallentamento delle prestazioni.
Garbage collector: le generazioni e come accelerare la pulizia
Come si diceva, questi processi sono solitamente abbastanza veloci, di solito durano meno di un paio di millisecondi, ma la velocità di esecuzione dipende anche dal tipo di memoria che viene ripulita, cioè di quale “generazione” faccia parte. Le generazioni delle memorie si dividono in:
- Generazione 0 o Gen0: è quella più giovane che contiene gli oggetti di breve durata, come le variabili temporanee
- Generazione 1 o Gen1: funge da buffer (“cuscinetto”) tra gli oggetti a breve durata e quelli a lungo termine, per questo quando un oggetto sopravvive a un tentativo di raccolta dei rifiuti viene promosso a una generazione superiore
- Generazione 2 o Gen2: tiene traccia degli oggetti a lungo termine.
Il garbage collector controllerà durante la sua operazione di pulizia gli oggetti in Gen0, Gen1 e Gen2. Per le loro caratteristiche, la pulizia di Gen0 e Gen1 sarà piuttosto veloce, dato che contiene oggetti di breve durata.
Quando però il sistema arriva a Gen2, che è solitamente costituita da un blocco molto più grande di memoria, ci potrebbero essere dei rallentamenti. La velocità di esecuzione di una pulizia, quindi, dipenderà anche dalle generazioni di memoria che vengono via via esaminate.
Per accelerare le prestazioni del garbage collector, un programmatore potrà cercare di produrre meno spazzatura possibile quando sviluppa il suo programma. Per farlo, potrà ad esempio usare un pool di oggetti, cioè dei valori predefiniti che saranno più veloci da ripristinare rispetto a creare e gettare oggetti a ogni esecuzione.