In JavaScript la gestione della memoria viene eseguita automaticamente ed è invisibile. Noi creiamo primitive, oggetti, funzioni⦠Tutte queste occupano memoria.
Cosa succede quando qualcosa non è più necessario? Come fa JavaScript a scoprirlo e pulirlo?
RaggiungibilitÃ
Il principale concetto della gestione della memoria in JavaScript è la raggiungibilità .
Semplicemente una valore âraggiungibileâ deve essere accessibile o utilizzabile. Questa proprietà ne garantisce la permanenza in memoria.
-
Câè una gruppo di valori che sono intrinsecamente raggiungibili, che non possono essere cancellati per ovvie ragioni.
Ad esempio:
- Funzioni in esecuzione, i loro parametri e le loro variabi locali.
- Funzioni che fanno parte della catena delle chiamate annidate, i loro parametri e le loro variabi locali.
- Variabili globali.
- (ce ne sono altri, anche interni)
Questi valori sono detti radici.
-
Qualsiasi altro valore viene considerato raggiungibile se è possibile ottenerlo per riferimento o per catena di riferimenti.
Ad esempio, se câè un oggetto in una variabile locale, e lâoggetto ha una proprietà che si riferisce ad un altro oggetto, questâultimo viene considerato raggiungibile. E anche tutti i suoi riferimenti lo saranno. Seguiranno esempi più dettagliati.
Câè un processo che lavora in background nel motore JavaScript, chiamato garbage collector. Monitora gli oggetti e rimuove quelli che sono diventati irraggiungibili.
Un semplice esempio
Qui un esempio molto semplice:
// user ha un riferimento all'oggetto
let user = {
name: "John"
};
Qui la freccia indica un riferimento ad un oggetto. La variabile globale "user" fa riferimento allâoggetto {name: "John"} (lo chiameremo John per brevità ). La proprietà "name" di John memorizza un tipo primitivo, quindi viene descritto allâinterno dellâoggetto.
Se il valore di user viene sovrascritto, il riferimento viene perso.
user = null;
Ora John diventa irraggiungibie. Non câè modo per accedervi, nessun riferimento. Il Garbage collector scarterà il dato per liberare la memoria.
Due riferimenti
Ora proviamo a pensare di aver copiato il riferimento da user su admin:
// user ha un riferimento all'oggetto
let user = {
name: "John"
};
let admin = user;
Ora se facciamo:
user = null;
â¦Lâoggetto rimane raggiungibile tramite admin, quindi è in memoria. Se sovrascriviamo anche admin, allora verrà rimosso.
Oggetti interconnessi
Ora vediamo un esempio più complesso. La famiglia:
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
La funzione marry âsposaâ due oggetti facendo in modo di fornire un riferimento lâuno per lâaltro, e ritorna un oggetto che li contiene entrambi.
La struttura della memoria risultante:
Per ora tutti gli oggetti sono raggiungibili.
Ora proviamo a rimuovere due riferimenti:
delete family.father;
delete family.mother.husband;
Non è sufficiente cancellare solo uno dei due riferimenti, perché lâoggetto rimarrebbe comunque raggiungibile.
Ma se li cancelliamo entrambi, allora John non ha più modo di essere raggiunto:
I riferimenti in uscita non contano. Solo quelli in entrata possono rendere lâoggetto raggiungibile. Quindi, John risulta ora irragiungibile e verrà quindi rimosso dalla memoria, come tutti i suoi dati visto che sono inaccessibili.
Dopo la pulizia del Garabage collector:
Isola irraggiungibile
Eâ possibile che lâintera isola degli oggetti collegati a vicenda diventi inaccessibile e venga quindi rimossa dalla memoria.
Lâoggetto sorgente è lo stesso di quello sopra. Poi:
family = null;
La memoria ora risulta cosi:
Questo esempio dimostra quanto sia importante il concetto della raggiungibilità .
Eâ ovvio che John e Ann siano ancora collegati, ma questo non è sufficiente.
Lâoggetto che li conteneva "family" è stato rimosso dalla radice, non esistono quindi dei rierimenti, lâisola diventa irraggiungibile e verrà cancellata.
Algoritmi interni
Lâalgoritmo basico utilizzato dal garbage collector viene chiamato âmark-and-sweepâ (segna e pulisci).
Vengono seguiti questi step per eseguire un processo di âgarbage collectionâ:
- Il garbage collector âmarchiaâ (ricorda) le radici.
- Successivamente visita e âmarchiaâ tutti i riferimenti contenuti.
- Successivamente visita gli oggetti marcati e marca i vari riferimenti. Tutti gli oggetti visitati vengono ricordati, cosi da non ricontrollarli nuovamente in futuro.
- â¦E cosi via fino ad aver controllato tutti i riferimenti (raggiungibili dalle radici).
- Tutti gli oggetti tranne quelli marcati vengono rimossi.
Ad esempio, rende la struttura del nostro oggetto del tipo:
Possiamo chiaramente vedere un âisola irraggiungibileâ nella parte desta. Ora vediamo come la gestisce lâalgoritmo âmark-and-sweepâ.
Il primo step sta nel marcare le radici:
Poi vengono marcari i loro riferimenti:
â¦E i loro riferimenti, finchè non si esauriscono:
Ora gli oggetti che non sono stati visitati vengono considerati irraggiungibili e verranno rimossi:
Questo è il concetto che sta dietro il funzionamento del Garbage collector.
JavaScript applica diverse ottimizzazioni per renderlo più rapido.
Alcune delle ottimizzazioni:
- Raggruppamento generazionale â gli oggetti vengono divisi in due gruppi: ânuoviâ e âvecchiâ. Molti oggetti vengono costruiti, eseguono il proprio lavoro e muoiono, quindi possono essere rimossi rapidamente. Quelli che vivono abbastanza al lungo vengono considerati come âvecchiâ e verranno controllati con minore intensitÃ
- Raggruppamento incrementale â se ci sono molti oggetti, e ci mettessimo a controllare interi gruppi per marcarli, si perderebbe molto tempo, questo ritardo diventerebbe visibile durante lâesecuzione. Quindi i motori JavaScript tentano di dividere il processo in diverse parti. Questi pezzi vengono controllati uno per uno, separatamente. Eâ richiesto lâutilizzo di un registro per tenere traccia dei cambiamenti, in cambio avremmo tanti piccoli ritardi piuttosto che uno singolo ma enorme.
- Raggruppamento per inattività â il garbage collector cerca di eseguire i suoi processi solo nei momenti in cui la CPU è inattiva, per ridurre al minimo possibile i ritardi durante lâesecuzione.
Ci sono altre ottimizzazioni per ottimizzare i processi del Garbage collector. Anche se mi piacerebbe poterli spiegare in dettaglio, sono costretto a fermarmi, poiché le varia ottimizzazioni dipendono dai motori che vengono utilizzati. Inoltre, i motori cambiano, si aggiornano e diventano sempre più âavanzatiâ. Quindi se siete realmente interessati, vi lascio qualche link sotto.
Riepilogo
Le principali cose da conoscere:
- Il processo di Garbage collection viene eseguito automaticamente. Non possiamo forzarlo o bloccarlo.
- Gli oggetti vengono mantenuti in memoria solo finché risultano raggiungibili.
- Essere riferimento di qualunque altro oggetto non significa essere raggiungibili (dalla radice): un gruppo di oggetti possono diventare irraggiungibili in un solo colpo.
I motori moderni applicano algoritmi avanzati di garbage collection.
Un buon libro âThe Garbage Collection Handbook: The Art of Automatic Memory Managementâ (R. Jones et al) che descrive alcuni degli algoritmi.
Se conoscete la programmazione a basso livello, informazioni più dettagliate riguardo il Garbage collector V8 sono disponibili nellâarticolo A tour of V8: Garbage Collection.
V8 blog pubblica articoli riguardo i cambiamenti nella gestione della memoria. Ovviamente per apprendere il processo di garbage collection, è fortemente consigliato imparare il funzionamento del garbage collector V8 leggendo il blog Vyacheslav Egorov che ha lavorato come ingegnere per lo sviluppo del V8. Vi dico âV8â perché è quello più utilizzato e maggiormente spiegato su internet. Per gli altri motori, gli approcci sono simili, ci sono alcune sottili differenze.
Le conoscenze profonde dei motori sono importanti quando necessitate di ottimizzazioni a basso livello. Potrebbe essere un buon traguardo dopo essere diventati familiari con il linguaggio.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)