garbage-collection garbage-collector

JavaScript: memoria e garbage collection

garbage-collector-javascript

Anche quando si lavora con linguaggi di alto livello quale JavaScript, gli sviluppatori dovrebbero avere almeno una comprensione di base della gestione della memoria, per affrontare correttamente il sopraggiungere di eventuali perdite e trovare un'alternativa che possa risolverle.

  1. Funzionamento della memoria
  2. Stack
  3. Heap
  4. Garbage collector: l'algoritmo "mark-and-sweep"
  5. Perdite di memoria in JavaScript: come risolverle
1.

Funzionamento della memoria

A prescindere dal linguaggio che si andrà ad utilizzare (di programmazione o di scripting), il ciclo di vita (trifasico) della memoria, sostanzialmente, è sempre uguale:

  1. allocazione: la memoria viene assegnata dal sistema operativo, consentendo al programma di utilizzarla. Nei linguaggi di medio-basso livello, come il C, questa è un'operazione esplicita, che lo sviluppatore dovrà gestire manualmente; in quelli di alto livello, invece, la gestione è automatica;
  2. utilizzo: è il momento in cui il programma usa effettivamente la memoria allocata in precedenza, determinando, l'esecuzione del codice, operazioni di lettura e scrittura su di essa.
  3. rilascio: è il momento in cui la memoria non più utilizzata viene liberata, in modo che possa diventare di nuovo disponibile. Come con l’allocazione, questa è un’operazione da compiere esplicitamente nei linguaggi di livello medio-basso.
ram-garbage-collection

A livello hardware, la memoria del computer è costituita da tanti transistor in grado di memorizzare un bit ed essere indirizzati da un identificatore univoco, quindi possiamo leggerli e sovrascriverli.
I bit sono organizzati in gruppi più grandi e usati per rappresentare i numeri; ad esempio, 8 bit fanno 1 byte.

2.

Stack

memoria-allocata

Quando si compila il codice, il compilatore esamina i tipi di dato e calcola in anticipo la quantità di memoria necessaria per l'esecuzione.
Il compilatore, che conosce l'esatto indirizzo di memoria di ciascun dato, interagirà con il sistema operativo per richiedere il numero di byte necessario a memorizzare le variabili.
L'importo richiesto viene quindi allocato in uno spazio di memoria denominato stack di chiamate, al cui interno vengono memorizzate variabili locali e chiamate di funzioni.
Quando le funzioni vengono chiamate ognuna riceve il proprio pezzo di stack, ovvero viene creata una zona di memoria al di sotto della quale vengono allocate le variabili locali della funzione invocata (comprese le variabili rappresentate dai parametri della funzione).
Di ogni funzione viene ricordato l'indirizzo, e quando termina l'esecuzione il suo blocco di memoria viene nuovamente e automaticamente reso disponibile per altri scopi.
Lo stack è utile per l’allocazione della memoria statica, il che significa che prima che il programma venga eseguito la memoria viene allocata in fase di compilazione.

3.

Heap

La faccenda si complica quando non si conosce, al momento della compilazione, la quantità di memoria di cui avrà bisogno una variabile, questo perché non può essere allocata nello stack, quindi il programma dovrà chiedere esplicitamente al sistema operativo la giusta quantità in fase di esecuzione.
Questa memoria viene assegnata sullo spazio heap, alle cui regioni allocate si accede per via indiretta, attraverso un "riferimento" proveniente dallo stack.

stack-heap

Attraverso la memoria heap è possibile accedere alle variabili a livello globale, che possono essere ridimensionate, contrariamente a quelle presenti sullo stack, i cui blocchi di memoria presentano dimensioni fisse, che non possono essere espanse o contratte; inoltre, nella heap vengono memorizzati i dati degli array e degli oggetti istanziati dal programma, ai quali si accede (come già accennato) mediante un riferimento sullo stack che "punta" ad essi (immagine sopra).
Nell’allocazione di memoria basata su heap (allocazione dinamica), la dimensione della memoria da allocare può essere determinata solo a runtime (cioè, in fase di esecuzione), crescendo o diminuendo secondo le nostre necessità, diversamente dallo stack in cui allocazione e liberazione non avvengono casualmente, ma in modo lineare e sequenziale.

Quanto finora descritto avviene anche in JavaScript, ma stack ed heap sono quelli della memoria del browser, non del sistema operativo, che comunque rimane coinvolto nella trattazione, considerato che il browser, al pari di altri software, gira su di esso.

Linguaggi come il C hanno primitive di gestione della memoria di basso livello, come malloc(), calloc(), realloc() e free(), che vengono utilizzate dallo sviluppatore per allocare, reallocare e deallocare la memoria per il sistema operativo.
Il sistema operativo non si occupa della gestione della heap quando si sviluppa in C, dunque allocazione e deallocazione della memoria all'interno di questo segmento sono a totale carico del programmatore.
Pertanto, ogni volta che viene allocata la memoria per mezzo delle funzioni sarà opportuno deallocarla quando non più necessaria.

4.

Garbage collector: l'algoritmo "mark-and-sweep"

Linguaggi come JavaScript, invece, sollevano gli sviluppatori dalla responsabilità di gestire le allocazioni di memoria, facendolo al posto loro in maniera invisibile.
Usare la memoria allocata significa leggere e scrivere dati su di essa, dichiarando variabili, definendo proprietà e metodi di un oggetto, passando argomenti a una funzione.
Questi dati, detti radici, sono raggiungibili (per valore o per riferimento), quindi accessibili e utilizzabili, ed è proprio la loro raggiungibilità a garantirne la permanenza in memoria.
Ma, quando i dati non vengono più utilizzati JS, automaticamente, libera la memoria, grazie a un processo chiamato garbage collection, che tiene traccia della memoria utilizzata, e quando ne trova una parte allocata non più necessaria la libera.
La memoria allocata non è più necessaria quando non è più possibile accedervi, perché ad esempio tutte le variabili che puntano ad essa sono uscite dallo scope (ovvero, distrutte al termine dell'esecuzione della funzione).

L’algoritmo utilizzato dal garbage collector viene chiamato "mark-and-sweep" (segna e pulisci).
Il garbage collector "marchia" e ricorda le radici e i vari riferimenti, cosi da non ricontrollarli nuovamente in futuro, e cosi via fino ad aver controllato tutti i riferimenti raggiungibili dalle radici: le radici che non sono state visitate vengono considerate irraggiungibili e verranno rimosse ("pulite").

garbage-collector

La precisione con cui lavora il GC, tuttavia, non è assoluta.
Non è possibile stabilire con certezza quando verrà eseguita una raccolta (pulizia della memoria), il che significa che in alcuni casi i programmi utilizzano più memoria di quella effettivamente necessaria.
In scenari applicativi delicati, brevi pause possono essere evidenti quando le performance risultano determinanti.

5.

Perdite di memoria in JavaScript: come risolverle

Le perdite di memoria sono pezzi di memoria che l'applicazione ha utilizzato in passato, ma non è più necessaria e non è ancora stata restituita al sistema operativo o al pool di memoria libera.
Ciò potrebbe far sì che il sistema operativo (o il browser, nel caso di JavaScript) allochi all'applicazione molta più memoria del necessario.
Questo consumo di memoria da parte dell'applicazione è progressivo, dunque continuerà ad aumentare fino a quando il sistema operativo non la esaurirà.
La comodità del GC porta a trascurare l'utilizzo della memoria, traducendosi spesso in pagine o applicazioni web che utilizzano molta più memoria di quanto dovrebbero realmente.

Quando si accede a una pagina o a un'applicazione web e le prestazioni si riducono progressivamente per un periodo di tempo, potrebbe essere indice di una perdita di memoria, ma non necessariamente; è un segno di elevato consumo di risorse, ma potrebbe anche trattarsi di latenza di rete (lag ) o di un elevato consumo di CPU dovuto all'apertura e al contemporaneo lavoro di troppi task sul sistema operativo.

A volte le perdite di memoria sono molto piccole e mostrano i loro effetti solo su lunghi periodi di tempo; a questo proposito, strumenti quali i Chrome Dev Tools sono in grado di intercettarle.
Una volta aperto il Chrome Inspector, andare alla scheda "Prestazioni", assicurarsi che la casella di controllo della "memoria" sia selezionata e avviare una registrazione.

chrome-dev-tools-performance

Eseguire l'azione che si sospetta stia causando la perdita di memoria, attendere un certo periodo di tempo (1-2 minuti) e interrompere la registrazione.
Esaminando il grafico della memoria noteremo una linea scalare in azzurro, che corrisponde alla Heap di JS (immagine sotto).
Normalmente, quando una pagina viene caricata, ci si dovrebbe aspettare un maggiore consumo all'inizio, durante l'allocazione, e poi un calo, che si verifica durante la garbage collection.
A caricamento completato, vedremo raggiunto un equilibrio in termini di consumo di memoria e il grafico oscillerà (nel periodo di tempo analizzato) tra un punto massimo e minimo.
Se questo è il grafico la pagina o web app non sta subendo perdite di memoria.

chrome-dev-tools-performance-memoria

Invece, una linea scalare a picchi progressivi è una chiara indicazione della presenza di una perdita di memoria nell'applicazione.
In questo caso, l'allocazione continua ad aumentare nel tempo finché le prestazioni si ridurranno in modo significativo, a conferma che la pagina o applicazione sta soffrendo una perdita.
A volte il grafico non è dirimente, pertanto potrebbe essere necessario registrare la performance più a lungo per vedere la tendenza nel lungo periodo.

chrome-dev-tools-memory-leak

I colori nel grafico della CPU corrispondono ai colori della scheda "Riepilogo", nella parte inferiore del pannello Prestazioni.
La presenza di molto colore significa che la CPU è stata esaurita durante la registrazione. Quando la CPU raggiunge il massimo per lunghi periodi significa che il consumo di memoria è troppo dispendioso.
Quando nessun evento è selezionato, la scheda "Riepilogo" mostra un'analisi dettagliata dell'attività complessiva.
Selezionando uno screenshot subito sopra il grafico della memoria, è possibile analizzare l'andamento del consumo di risorse impostando un range temporale ancora più specifico; oppure, dalla sezione "Principale" ("Main"), selezionando le barre grigie più ampie e marchiate con dei triangoli rossi, è possibile concentrarsi sugli eventi che hanno richiesto più tempo, detti anche "colli di bottiglia" (bottleneck ).
Le schede Riepilogo ("Summary") e Dal basso verso l'alto ("Bottom-Up") visualizzeranno solo le informazioni per la parte selezionata della registrazione, mostrando quali task hanno contribuito maggiormente all'attività "lunga", facendole impiegare un tempo eccessivo per il completamento, compreso un link che permette di saltare alla riga pertinente nel codice sorgente responsabile del funzionamento del task.

A questo proposito, la guida di PAN Web Design alla comprensione delle metriche rilevate dall'audit tool PageSpeed Insights di Google (raggiungibile al link seguente Come velocizzare il caricamento delle pagine web), contiene svariati suggerimenti su come ottimizzare e velocizzare le prestazioni di JavaScript (proprio e di terze parti).


Privacy Policy