Comprendere la Garbage Collection in JavaScript
La garbage collection è una funzione di gestione automatica della memoria che garantisce un uso efficiente della memoria recuperando quella occupata da oggetti non più necessari.
Introduzione alla Garbage Collection
La garbage collection gestisce automaticamente la memoria in JavaScript. Libera la memoria utilizzata da dati non più necessari, in modo che tu non debba quasi mai allocare o rilasciare memoria manualmente. Questa pagina spiega il concetto su cui si basa l'intero sistema — la raggiungibilità — e poi illustra l'algoritmo mark-and-sweep, le perdite di memoria che possono ancora verificarsi e come WeakMap/WeakSet aiutano ad evitarle.
Comprendere questo aspetto è importante perché "automatico" non significa "privo di perdite". Il collector rimuove solo ciò che riesce a dimostrare essere irraggiungibile; se il tuo codice mantiene vivo un riferimento nascosto, la memoria rimane allocata per tutta la durata del programma.
Raggiungibilità: il concetto fondamentale
Il motore non tiene traccia del fatto che un object sia "in uso" in senso logico. Tiene traccia di se l'object è raggiungibile — ovvero se esiste una catena di riferimenti che porta ad esso da una radice.
Le radici sono valori che il motore mantiene sempre:
- Le variabili locali e i parametri della funzione in esecuzione.
- Le variabili e le funzioni nella catena corrente di chiamate nidificate.
- Le variabili globali (proprietà di
globalThis/window).
Qualsiasi object raggiungibile seguendo i riferimenti da una radice — direttamente o attraverso altri object raggiungibili — viene mantenuto. Tutto il resto è spazzatura.
Esempio: un riferimento mantiene vivo un object
Spiegazione: L'object { name: "John" } era raggiungibile attraverso user. Copiando il riferimento in admin si crea un secondo percorso verso di esso. Impostando user = null si rimuove un percorso, ma admin punta ancora all'object, che rimane raggiungibile e non viene raccolto.
Gli object interconnessi vengono comunque raccolti
Un mito comune è che gli object che si riferiscono l'uno all'altro sopravvivano. Non è così — ciò che conta è la raggiungibilità da una radice, non il fatto che gli object si puntino a vicenda.
Spiegazione: obj1 e obj2 formano un ciclo, ma una volta che entrambe le variabili radice sono impostate a null non esiste alcun percorso da alcuna radice al ciclo. L'intera isola diventa irraggiungibile e idonea alla raccolta. È per questo che i motori JavaScript usano la raggiungibilità invece del semplice reference counting, che causerebbe perdite di memoria con i cicli.
Come funziona il collector: mark-and-sweep
I motori JavaScript recuperano la memoria con l'algoritmo mark-and-sweep. Riduce "questo object non è più necessario" alla domanda precisa "questo object non è più raggiungibile."
- Mark. Partendo dalle radici, il collector visita ogni object raggiungibile e lo contrassegna. Poi segue i loro riferimenti, contrassegna quegli object e così via finché tutti gli object raggiungibili non sono contrassegnati.
- Sweep. Ogni object che non è stato contrassegnato è irraggiungibile. La memoria che occupa viene liberata.
Non puoi attivare questo processo manualmente e non dovresti provarci — non esiste un gc() standard nel linguaggio. I motori reali (come V8) affinano l'algoritmo di base con ottimizzazioni come la raccolta generazionale (i nuovi object muoiono giovani, quindi vengono controllati più spesso) e la raccolta incrementale (il lavoro viene diviso in blocchi per evitare pause). Il modello di raggiungibilità su cui hai ragionato sopra rimane invariato.
Fonti comuni di perdite di memoria
Una perdita in JavaScript è semplicemente un object che rimane raggiungibile anche se il programma ha finito di usarlo. Il collector funziona correttamente — semplicemente non riesce a capire che il riferimento è obsoleto. Fai attenzione a questi pattern.
Timer e intervalli dimenticati
Un setInterval (o setTimeout) in attesa mantiene vivo il suo callback, e il callback mantiene vivo tutto ciò che racchiude nella closure. Se non chiami mai clearInterval, quella memoria viene trattenuta per tutta la durata della pagina.
Nodi DOM staccati
Se rimuovi un elemento dalla pagina ma mantieni un riferimento ad esso in una variabile, il nodo — e l'intera sua struttura ad albero — non può essere raccolto.
let detached = document.getElementById('list');
document.body.removeChild(detached);
// The node is gone from the page, but 'detached' still references it,
// so it stays in memory. Release it when done:
detached = null;Event listener persistenti
Un listener associato a un elemento DOM mantiene sia l'elemento che il gestore (con tutto ciò che racchiude nella closure) raggiungibili. Rimuovi i listener con removeEventListener una volta che non sono più necessari:
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
function alertClick() {
alert("Button clicked!");
button.removeEventListener('click', alertClick); // free the listener after first use
}
button.addEventListener('click', alertClick);
</script>Il pulsante risponde solo al primo clic: il gestore si rimuove con removeEventListener, rilasciando il riferimento in modo che possa essere sottoposto a garbage collection.
Cache globali che crescono solo
Una cache memorizzata in un object a livello di modulo o globale mantiene ogni voce raggiungibile per sempre, a meno che tu non elimini esplicitamente quelle vecchie. Una Map illimitata usata come cache è una classica perdita lenta.
Closure che catturano più di quanto si pensi
Una closure mantiene vive tutte le variabili negli scope a cui fa riferimento — anche quelle che non usa mai effettivamente. Restituire una piccola funzione interna da una funzione con variabili locali di grandi dimensioni può bloccare quelle variabili locali in memoria. Mantieni lo scope catturato al minimo e consulta variable scope per capire come le closure mantengono il loro ambiente.
Come aiutano WeakMap e WeakSet
Map e Set mantengono riferimenti forti alle loro chiavi/valori, quindi tutto ciò che è memorizzato in esse rimane raggiungibile. WeakMap e WeakSet mantengono le loro chiavi debolmente: se l'unico riferimento rimanente a un object è quello all'interno di una WeakMap, l'object può comunque essere raccolto, e la voce scompare con esso.
Questo rende WeakMap ideale per associare dati extra agli object (cache, metadati, gestione dei nodi DOM) senza costringerli a vivere per sempre. Poiché le voci possono scomparire in qualsiasi momento, WeakMap/WeakSet non sono deliberatamente iterabili e non hanno size.
Best practice
- Preferisci le variabili locali; escono automaticamente dallo scope e diventano raccoglibili quando la funzione ritorna.
- Limita le variabili globali — vivono per tutta la durata dell'applicazione. Usa moduli e scope di blocco (
let/const). - Cancella i timer (
clearInterval/clearTimeout) e rimuovi gli event listener conremoveEventListenernon appena non sono più necessari. - Imposta a null i riferimenti a nodi DOM staccati e altri oggetti di grandi dimensioni di cui hai finito l'uso.
- Usa
WeakMap/WeakSetper cache e metadati indicizzati per object, così le voci non sopravvivono alle loro chiavi.
Conclusione
La garbage collection in JavaScript è costruita interamente sulla raggiungibilità: il motore mantiene qualsiasi object che riesce a raggiungere da una radice e libera il resto usando mark-and-sweep, che gestisce anche i cicli di riferimento. "Automatico" non ti esonera comunque dal non mantenere riferimenti obsoleti — timer dimenticati, nodi DOM staccati, cache in crescita continua e closure che catturano troppo sono i soliti colpevoli. Usa WeakMap e WeakSet quando vuoi associare dati agli object senza mantenerli in vita.