4 tipi di perdite di memoria in JavaScript e come eliminarle 1/2

b
Scopri le perdite di memoria in JavaScript e cosa puoi fare per risolverle!
In questo articolo esploreremo le tipologie di memory leak nel codice JavaScript lato client. Impareremo anche come utilizzare gli strumenti per sviluppatori di Chrome per trovarli. Iniziamo!

Introduzione

Le perdite di memoria sono un problema che devono affrontare tutti gli sviluppatori, anche quando lavorano con linguaggi che gestiscono la memoria, ci sono casi in cui si verificano perdite di memoria. Le perdite sono la causa di un'intera famiglia di problemi: rallentamenti, arresti anomali, tempi di risposta elevati e persino problemi con altre applicazioni.

Che cos'è una perdita di memoria?

Una perdita di memoria può essere essenzialmente definita come un'area di memoria che non è più utilizzata da un'applicazione ma che non viene liberata e quindi non resa disponibile al sistema operativo. Le lingue hanno diversi metodi di gestione della memoria. Questi metodi riducono le possibilità che si verifichi una perdita di memoria. Tuttavia, sapere se un'area di memoria è diventata inutile o meno è un problema potenzialmente insolubile. Solo i progettisti sono in grado di chiarire se un'area di memoria non è utilizzata. Wikipedia ha buoni articoli sulla gestione manuale e automatica della memoria.

Gestione della memoria in JavaScript

JavaScript è una delle lingue con Garbage Collection. I linguaggi di Garbage Collection aiutano gli sviluppatori a gestire la memoria controllando periodicamente quale area di memoria precedentemente allocata dovrebbe rimanere accessibile da altre parti dell'applicazione. In altre parole, i linguaggi basati sulla garbage collection riducono il problema di gestione della memoria da "quale area di memoria è ancora necessaria" a "quale area di memoria rimane accessibile al resto dell'applicazione". La differenza è sottile ma importante: mentre solo lo sviluppatore sa quale area di memoria dovrebbe rimanere accessibile in futuro, le aree di memoria che sono diventate inaccessibili possono essere identificate algoritmicamente e contrassegnate per essere restituite al sistema operativo.

I linguaggi garbage-free in genere utilizzano altre tecniche per gestire la memoria: gestione esplicita della memoria, in cui gli sviluppatori comunicano al compilatore quando un'area di memoria non è più necessaria, e conteggio dei riferimenti, in cui a ciascun blocco di memoria è associato un conteggio di utilizzo (quando raggiunge lo zero, l'area viene restituita al sistema operativo). Queste tecniche hanno le loro controparti (e potenziali rischi di perdite).

Perdite in JavaScript

Il motivo principale delle perdite nei linguaggi di Garbage Collection sono i riferimenti indesiderati. Per capire quali sono i riferimenti indesiderati, dobbiamo prima capire come il Garbage Collector determina se un'area di memoria è accessibile o meno.

"Il motivo principale delle perdite nei linguaggi basati su Garbage Collector sono i riferimenti indesiderati"

 

Segna e spazza

La maggior parte dei Garbage Collector utilizza un algoritmo chiamato mark-and-sweep. L'algoritmo è composto dai seguenti passaggi:

  1. Il Garbage Collector crea un elenco di "radici". Le radici sono in genere variabili globali a cui si fa riferimento nel codice. In JavaScript, l'oggetto "windows" è un esempio di una variabile globale che può fungere da root. L'oggetto Windows è sempre presente, quindi Garbage Collector può considerarlo - e tutti i suoi figli - sempre presenti (cioè, non Garbage).
  2. Tutte le radici vengono ispezionate e contrassegnate come attive. Tutti i bambini vengono anche ispezionati in modo ricorsivo. Tutto ciò a cui è possibile accedere da root non è considerato spazzatura.
  3. Tutte le aree di memoria che non sono contrassegnate come attive possono quindi essere considerate spazzatura. Il servizio di raccolta può quindi rilasciare queste aree di memoria e restituirle al sistema operativo.

La maggior parte dei moderni Garbage Collector si basa su questo algoritmo con variazioni, ma il principio rimane lo stesso: tutte le aree di memoria accessibili sono contrassegnate come tali e il resto è considerato spazzatura.
I riferimenti indesiderati sono riferimenti ad aree di memoria che lo sviluppatore non sa più utilizzare ma che, per tutta una serie di motivi, rimangono nell'albero di una radice attiva. Nel contesto di JavaScript, i riferimenti indesiderati sono variabili che vengono mantenute da qualche parte nel codice che non verranno più utilizzate e puntano a un'area di memoria che potrebbe essere stata liberata. Per alcuni, è un errore dello sviluppatore.
Per capire quali sono le perdite più comuni in JavaScript, devi guardare come spesso i riferimenti vengono dimenticati.
a

I quattro tipi di perdite JavaScript:

1: variabili globali accidentali

Uno degli obiettivi di JavaScript era sviluppare un linguaggio che assomigliasse a Java ma che fosse abbastanza permissivo da poter essere utilizzato dai principianti. Ciò si riflette nel modo in cui JavaScript gestisce le variabili non dichiarate: un riferimento a una variabile non dichiarata crea una nuova variabile nell'oggetto globale. Nel caso di un browser, l'oggetto globale è finestra. In altre parole:
function foo(arg) {
bar = "this is a hidden global variable";
}
Is in fact:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
Se la barra doveva contenere il riferimento a una variabile solo all'interno dell'ambito della funzione foo e ti dimentichi di dichiararla, una variabile globale viene creata per caso. In questo esempio, perdere una stringa non è molto pericoloso, ma potrebbe essere molto peggio.
Un altro modo per creare accidentalmente una variabile globale è questo:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

Per evitare che questi errori vengano visualizzati, aggiungi 'use strict'; all'inizio dei tuoi file JavaScript. Ciò attiva una modalità di analisi JavaScript più rigorosa che impedisce i globali per errore.

Nota sulle variabili globali
Sebbene si parli di globali insospettabili, la realtà è che molto codice è disseminato di variabili globali esplicite. Sono per definizione inesigibili (a meno che non siano annullati o riassegnati). In particolare, destano preoccupazione le variabili globali utilizzate per archiviare ed elaborare grandi quantità di informazioni. Se devi utilizzare variabili globali per archiviare molti dati, assicurati di annullarle o riassegnarle quando hai finito.
Una causa comune di maggiore consumo di memoria in relazione a Globals è la memorizzazione nella cache. La cache memorizza i dati che vengono utilizzati ripetutamente. Affinché ciò sia efficace, la cache deve avere un limite di dimensioni maggiori. Le cache che crescono senza limiti comportano un consumo di memoria elevato perché non è possibile raccogliere il contenuto.

2: timer o richiamate dimenticati

L'uso di setInterval è abbastanza comune in JavaScript. Le altre librerie forniscono osservatori e altri sistemi che supportano i callback. La maggior parte di queste librerie rende i callback inaccessibili dopo che le loro istanze stesse diventano inaccessibili. Nel caso di setInterval, è comune vedere codice come questo:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
Questo esempio illustra cosa può accadere con i timer penzolanti: timer che fanno riferimento a un nodo o dati che non sono più necessari. L'oggetto rappresentato dal nodo potrebbe essere cancellato in futuro, rendendo inutilizzabile l'intero blocco all'interno del range handler.
Tuttavia, il gestore, proprio come l'intervallo, è ancora attivo e quindi non può essere raccolto (per questo, l'intervallo dovrebbe essere interrotto). Se l'intervallo non può essere raccolto, nemmeno le sue dipendenze. Ciò significa che non è possibile raccogliere nemmeno le risorse, che memorizzano dati potenzialmente abbastanza sostanziali.
Nel caso degli osservatori, è importante rendere esplicite le chiamate per rimuoverli quando non sono più necessari (o gli oggetti associati stanno per diventare irraggiungibili). In passato, questo era particolarmente importante poiché alcuni browser (IE6) non erano in grado di gestire bene i riferimenti ciclici (vedi sotto per maggiori informazioni su questo)
Oggi, la maggior parte dei browser può e raccoglierà gestori di osservatori una volta che l'oggetto osservato diventa irraggiungibile. È buona norma, tuttavia, eliminare gli osservatori prima che l'oggetto venga eliminato. Per esempio :
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
Nota sugli osservatori di oggetti e sui riferimenti ciclici
Gli osservatori e i riferimenti ciclici sono stati a lungo l'incubo di uno sviluppatore JavaScript. Ciò era dovuto a un bug (o decisione di progettazione) nel Garbage Collector di Internet Explorer. Le versioni precedenti di IE non potevano rilevare i riferimenti ciclici tra i nodi DOM e il codice JavaScript. Questo è caratteristico degli osservatori, che generalmente mantengono un riferimento all'osservabile (come nell'esempio sopra). In altre parole, ogni volta che un osservatore viene aggiunto a un nodo in Internet Explorer, si verifica una perdita di memoria. Ecco perché gli sviluppatori hanno iniziato a rimuovere manualmente i gestori prima dei nodi o ad annullare i riferimenti negli osservatori. Oggi i browser moderni (inclusi Internet Explorer e Microsoft Edge) utilizzano algoritmi di Garbage Collection più moderni in grado di rilevare questi cicli e gestirli correttamente. In altre parole, non è più necessario chiamare removeEventListener prima di rendere un nodo irraggiungibile.
Framework come jQuery rimuovono i listener prima di rilasciare i nodi (quando usano la loro API specifica per quello). Viene gestito internamente dalla libreria e garantisce che non generi perdite, anche su browser meno recenti come il vecchio Internet Explorer.

3: Riferimenti al di fuori del DOM

A volte può essere utile memorizzare i nodi DOM nelle strutture dati. Si supponga di voler aggiornare rapidamente il contenuto di più colonne in una tabella. Potrebbe avere senso memorizzare un riferimento a ciascuna colonna DOM in un dizionario o in un array. Quando ciò accade, vengono mantenuti due riferimenti allo stesso elemento DOM: uno nell'albero DOM e l'altro nel dizionario. Se in futuro deciderai di eliminare queste colonne, dovrai rendere inaccessibili entrambi i riferimenti.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
Un'altra cosa da considerare sono i riferimenti ai nodi interni o alle foglie dell'albero DOM. Si supponga, ad esempio, di mantenere un riferimento a una cella di tabella specifica (a tag) nel codice JavaScript. Ad un certo punto, decidi di rimuovere la tabella specifica dal DOM ma di mantenere il riferimento a quella cella. Intuitivamente, si potrebbe pensare che il GC raccoglierà correttamente tutto tranne questa cella. In pratica, questo non accadrà: questa cella è un nodo figlio dell'array e i figli mantengono i riferimenti ai loro genitori. Ciò significa che l'intera tabella rimarrà in memoria, a causa del riferimento JavaScript a questa cella. Tienine conto se mantieni i riferimenti agli elementi DOM.

4: Chiusura

Un elemento chiave dello sviluppo di JavaScript è la chiusura: funzioni anonime che acquisiscono variabili dall'ambito padre. Gli sviluppatori di Meteor hanno riscontrato un caso speciale, piuttosto sottile, di perdita di memoria a causa dei dettagli dell'implementazione del runtime di Javascript:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
Questo frammento fa una cosa: ogni volta sostituire cosa è chiamato, la cosa recupera un nuovo oggetto che contiene un array di grandi dimensioni e una nuova chiusura (qualche metodo). Allo stesso tempo, la variabile inutilizzata contiene un recinto a cui fa riferimento cosa originale (la cosa dalla precedente chiamata a sostituire cosa). Già un po' complicato, giusto? L'importante è che una volta creato l'ambito per i fence, che si trovano nello stesso ambito padre, questo ambito sia condiviso.
In questo caso, l'ambito creato per la chiusura qualche metodo è condiviso con non usato. non usato ha un riferimento a cosa originale. sebbene non usato non essere mai usato. qualche metodo può essere utilizzato attraverso la cosa. E come qualche metodo condivide l'ambito della recinzione con non usato, anche se non usato non viene mai utilizzato, il suo riferimento a cosa originale lo costringe a rimanere attivo (impedisce la sua raccolta).
Quando questo frammento viene eseguito ripetutamente, vediamo un aumento costante dell'utilizzo della memoria. E non si riduce con il passaggio del CG. In sostanza, viene creato un elenco collegato di chiusura (radicato dalla variabile la cosa) e ogni ambito di tali chiusure contiene un riferimento all'ampio array, che genera una conseguente perdita.
Questo è un artefatto di implementazione. Un'altra implementazione di recinzioni in grado di gestire questo problema è concepibile come spiegato nel meteor-blog.
Articolo originale de Sebastian Peyrot tradotto da JS Staff
 
Non perdere il seguito del prossimo episodio: come riparare le perdite di memoria 🙂
[tipo separatore=”” size=”” icon=”stella”] [actionbox color=”default” title=”” description=”JS-REPUBLIC è una società di servizi specializzata nello sviluppo di JavaScript. Siamo un centro di formazione riconosciuto. Trova tutta la nostra formazione tecnica sul nostro sito partner dedicato alla Formazione” btn_label=”La nostra formazione” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external =”1″]