Questa sezione descrive i termini comuni utilizzati nell'analisi della memoria ed è applicabile a una serie di strumenti di profilazione della memoria per lingue diverse.
I termini e le nozioni descritti qui fanno riferimento a Chrome DevTools Heap Profiler. Se hai mai lavorato con Java, .NET o qualche altro profiler della memoria, questa potrebbe essere un'occasione per ripassare.
Dimensioni degli oggetti
Pensa alla memoria come a un grafo con tipi primitivi (come numeri e stringhe) e oggetti (array associativi). Può essere rappresentato visivamente come un grafico con una serie di punti interconnessi come segue:
Un oggetto può occupare memoria in due modi:
- Direttamente dall'oggetto stesso.
- Implicitamente, mantenendo riferimenti ad altri oggetti e impedendo quindi che questi oggetti vengano gestiti automaticamente da un garbage collector (GC per abbreviazione).
Quando utilizzi lo strumento Heap Profiler in DevTools (uno strumento per esaminare i problemi di memoria rilevati nel riquadro Memoria), probabilmente dovrai esaminare alcune colonne di informazioni diverse. Due di queste sono Dimensioni ridotte e Dimensioni conservate, ma cosa rappresentano?
Dimensioni poco profonde
Si tratta della dimensione della memoria occupata dall'oggetto stesso.
Gli oggetti JavaScript tipici hanno una certa memoria riservata per la loro descrizione e per lo stoccaggio di valori immediati. In genere, solo gli array e le stringhe possono avere dimensioni ridotte significative. Tuttavia, le stringhe e gli array esterni hanno spesso la loro memoria principale nella memoria del renderer, esponendo solo un piccolo oggetto wrapper nell'heap di JavaScript.
La memoria del renderer è tutta la memoria del processo in cui viene visualizzata una pagina ispezionata: memoria nativa + memoria heap JS della pagina + memoria heap JS di tutti i worker dedicati avviati dalla pagina. Tuttavia, anche un oggetto di piccole dimensioni può occupare indirettamente una grande quantità di memoria, impedendo che altri oggetti vengano eliminati dal processo di raccolta automatica dei rifiuti.
Dimensioni trattenute
Si tratta delle dimensioni della memoria che viene liberata una volta eliminato l'oggetto stesso insieme agli oggetti dipendenti che sono stati resi non raggiungibili dalle radici GC.
Le radici GC sono costituite da handle creati (locali o globali) quando si fa riferimento dal codice nativo a un oggetto JavaScript esterno a V8. Tutti questi handle sono disponibili in uno snapshot dell'heap in Radici GC > Ambito handle e Radici GC > Handle globali. Descrivere i handle in questa documentazione senza addentrarsi nei dettagli dell'implementazione del browser potrebbe creare confusione. Non devi preoccuparti di né delle radici GC né degli handle.
Esistono molte radici GC interne, la maggior parte delle quali non è interessante per gli utenti. Dal punto di vista delle applicazioni, esistono i seguenti tipi di radici:
- Oggetto Window globale (in ogni iframe). Negli snapshot dell'heap è presente un campo di distanza che corrisponde al numero di riferimenti alle proprietà nel percorso di conservazione più breve dalla finestra.
- Albero DOM del documento costituito da tutti i nodi DOM nativi raggiungibili percorrendo il documento. Non tutti possono avere wrapper JS, ma se li hanno, questi saranno attivi finché il documento è attivo.
- A volte gli oggetti possono essere conservati dal contesto del debugger e dalla console DevTools (ad esempio dopo la valutazione della console). Crea snapshot dell'heap con una console chiara e senza punti di interruzione attivi nel debugger.
Il grafico della memoria inizia con un elemento radice, che può essere l'oggetto window
del browser o l'oggetto Global
di un modulo Node.js. Non controlli il modo in cui questo oggetto principale viene eliminato da Google.
Tutto ciò che non è raggiungibile dalla radice viene sottoposto a GC.
Albero di conservazione degli oggetti
L'heap è una rete di oggetti interconnessi. Nel mondo matematico, questa struttura è chiamata grafo o grafo della memoria. Un grafo è costituito da nodi collegati tramite archi, a ciascuno dei quali vengono assegnate etichette.
- I nodi (o gli oggetti) sono etichettati utilizzando il nome della funzione costruttore utilizzata per crearli.
- I bordi sono etichettati utilizzando i nomi delle proprietà.
Scopri come registrare un profilo utilizzando Heap Profiler. Alcuni degli elementi più interessanti che possiamo vedere nella registrazione di Heap Profiler che segue includono la distanza: la distanza dalla radice del GC. Se quasi tutti gli oggetti dello stesso tipo si trovano alla stessa distanza e alcuni sono più lontani, vale la pena approfondire.
Dominator
Gli oggetti Dominator sono costituiti da una struttura ad albero perché ogni oggetto ha esattamente un Dominator. Un dominatore di un oggetto potrebbe non avere riferimenti diretti a un oggetto che domina; ovvero, l'albero del dominatore non è un albero espanso del grafo.
Nel seguente diagramma:
- Il nodo 1 domina il nodo 2
- Il nodo 2 domina i nodi 3, 4 e 6
- Il nodo 3 domina il nodo 5
- Il nodo 5 domina il nodo 8
- Il nodo 6 domina il nodo 7
Nell'esempio seguente, il nodo #3
è il dominatore di #10
, ma #7
esiste anche in ogni percorso semplice
da GC a #10
. Pertanto, un oggetto B è un dominatore di un oggetto A se B esiste in ogni percorso semplice dalla radice all'oggetto A.
Specifiche di V8
Quando esegui il profiling della memoria, è utile capire perché gli snapshot dell'heap hanno un aspetto specifico. Questa sezione descrive alcuni argomenti relativi alla memoria che corrispondono specificamente alla macchina virtuale JavaScript V8 (VM V8 o VM).
Rappresentazione di oggetti JavaScript
Esistono tre tipi primitivi:
- Numeri (ad es. 3.14159..)
- Valori booleani (true o false)
- Stringhe (ad es. 'Werner Heisenberg')
Non possono fare riferimento ad altri valori e sono sempre nodi foglia o terminali.
I numeri possono essere archiviati come:
- un valore intero immediato a 31 bit chiamato piccoli numeri interi (SMIs) oppure
- oggetti heap, denominati numeri heap. I numeri dell'heap vengono utilizzati per memorizzare valori che non rientrano nel formato SMI, ad esempio i double, o quando un valore deve essere incapsulato, ad esempio per impostare le proprietà.
Le stringhe possono essere memorizzate in:
- l'heap della VM.
- esternamente nella memoria del renderer. Viene creato e utilizzato un oggetto wrapper per accedere allo spazio di archiviazione esterno in cui, ad esempio, vengono archiviate le origini dello script e altri contenuti ricevuti dal web anziché copiati nell'heap della VM.
La memoria per i nuovi oggetti JavaScript viene allocata da un heap JavaScript dedicato (o heap VM). Questi oggetti sono gestiti dal garbage collector di V8 e, pertanto, rimarranno attivi finché esiste almeno un riferimento diretto a loro.
Gli oggetti nativi sono tutti gli altri oggetti che non si trovano nell'heap di JavaScript. L'oggetto nativo, diversamente dall'oggetto heap, non viene gestito dal garbage collector V8 per tutta la sua durata e può essere acceduto solo da JavaScript utilizzando l'oggetto wrapper JavaScript.
La stringa Cons è un oggetto costituito da coppie di stringhe memorizzate e poi unite ed è il risultato della concatenazione. L'unione dei contenuti della stringa cons avviene solo se necessario. Un esempio è quando è necessario creare una sottostringa di una stringa unita.
Ad esempio, se concatenate a e b, ottenete una stringa (a, b) che rappresenta il risultato della concatenazione. Se in un secondo momento concatenate d con questo risultato, ottenete un'altra stringa cons ((a, b), d).
Array: un array è un oggetto con chiavi numeriche. Vengono utilizzati ampiamente nella VM V8 per memorizzare grandi quantità di dati. Gli insiemi di coppie chiave-valore utilizzati come dizionari sono supportati da array.
Un tipico oggetto JavaScript può essere uno dei due tipi di array utilizzati per l'archiviazione:
- proprietà denominate e
- elementi numerici
Se il numero di proprietà è molto ridotto, possono essere archiviate internamente nell'oggetto JavaScript stesso.
Map: un oggetto che descrive il tipo di oggetto e il relativo layout. Ad esempio, le mappe vengono utilizzate per descrivere le gerarchie di oggetti implicite per l'accesso rapido alle proprietà.
Gruppi di oggetti
Ogni gruppo di oggetti nativi è costituito da oggetti che contengono riferimenti reciproci. Considera, ad esempio, un sottoalbero DOM in cui ogni nodo ha un link al nodo principale e ai nodi figlio e fratello successivo, formando così un grafo connesso. Tieni presente che gli oggetti nativi non sono rappresentati nell'heap di JavaScript, motivo per cui hanno dimensioni pari a zero. Vengono invece creati oggetti wrapper.
Ogni oggetto wrapper contiene un riferimento all'oggetto nativo corrispondente, per reindirizzare i comandi. A sua volta, un gruppo di oggetti contiene oggetti wrapper. Tuttavia, non viene creato un ciclo non raccoglibile, poiché GC è abbastanza intelligente da rilasciare gruppi di oggetti di cui non viene più fatto riferimento ai wrapper. Tuttavia, se dimentichi di rilasciare un singolo wrapper, verrà bloccato l'intero gruppo e i wrapper associati.