En esta sección, se describen los términos comunes que se usan en el análisis de memoria y que se aplican a una variedad de herramientas de generación de perfiles de memoria para diferentes lenguajes.
Los términos y las nociones que se describen aquí se refieren al Generador de perfiles de montón de las Herramientas para desarrolladores de Chrome. Si alguna vez trabajaste con Java, .NET o algún otro generador de perfiles de memoria, esto puede ser un repaso.
Tamaños de objetos
Piensa en la memoria como un grafo con tipos primitivos (como números y cadenas) y objetos (arrays asociativos). Visualmente, se podría representar como un gráfico con varios puntos interconectados de la siguiente manera:
Un objeto puede retener memoria de dos maneras:
- Directamente, por el objeto en sí.
- Implícitamente reteniendo referencias a otros objetos y, por lo tanto, evitando que el recolector de elementos no utilizados (GC) los elimine automáticamente.
Cuando trabajes con el Generador de perfiles de montón en Herramientas para desarrolladores (una herramienta para investigar problemas de memoria que se encuentra en "Perfiles"), es probable que veas algunas columnas diferentes de información. Dos que se destacan son Shallow Size y Retained Size, pero ¿qué representan?
Tamaño aplanado
Este es el tamaño de la memoria que retiene el objeto en sí.
Los objetos típicos de JavaScript tienen algo de memoria reservada para su descripción y para almacenar valores inmediatos. Por lo general, solo las matrices y las cadenas pueden tener un tamaño superficial significativo. Sin embargo, las cadenas y los arrays externos a menudo tienen su almacenamiento principal en la memoria del procesador, lo que expone solo un objeto wrapper pequeño en el montón de JavaScript.
La memoria del renderizador es toda la memoria del proceso en el que se renderiza una página inspeccionada: memoria nativa + memoria del montón de JS de la página + memoria del montón de JS de todos los trabajadores dedicados que inició la página. Sin embargo, incluso un objeto pequeño puede retener una gran cantidad de memoria de forma indirecta, ya que evita que el proceso automático de recolección de elementos no utilizados elimine otros objetos.
Tamaño retenido
Este es el tamaño de la memoria que se libera una vez que se borra el objeto junto con los objetos dependientes a los que no se puede acceder desde las raíces del GC.
Las raíces del GC se componen de controladores (ya sean locales o globales) cuando se hace una referencia desde el código nativo a un objeto de JavaScript fuera de V8. Todos estos controladores se pueden encontrar en una instantánea del montón en GC roots > Handle scope y GC roots > Global handle. Describir los controladores en esta documentación sin profundizar en los detalles de la implementación del navegador puede ser confuso. No debes preocuparte por las raíces del GC ni los controladores.
Existen muchas raíces de GC internas, la mayoría de las cuales no son interesantes para los usuarios. Desde el punto de vista de las aplicaciones, existen los siguientes tipos de raíces:
- Objeto global de la ventana (en cada iframe). Existe un campo de distancia en las instantáneas de montón que es la cantidad de referencias de propiedades en la ruta de retención más corta desde la ventana.
- Árbol del DOM del documento que consiste en todos los nodos nativos del DOM a los que se puede acceder recorriendo el documento. Es posible que no todos tengan wrappers de JS, pero si los tienen, estarán activos mientras el documento esté activo.
- A veces, el contexto del depurador y la consola de Herramientas para desarrolladores pueden retener objetos (p.ej., después de la evaluación de la consola). Crea instantáneas de montón con una consola limpia y sin puntos de interrupción activos en el depurador.
El gráfico de la memoria comienza con una raíz, que puede ser el objeto window
del navegador o el objeto Global
de un módulo de Node.js. No controlas cómo se realiza la recolección de elementos no utilizados de este objeto raíz.
Lo que no se puede alcanzar desde la raíz se recolecta como resultado de la recolección de elementos no utilizados.
Árbol de retención de objetos
El montón es una red de objetos interconectados. En el mundo matemático, esta estructura se denomina gráfico o gráfico de memoria. Un grafo se construye a partir de nodos conectados por medio de aristas, a los que se les asignan etiquetas.
- Los nodos (u objetos) se etiquetan con el nombre de la función de constructor que se usó para compilarlos.
- Los bordes se etiquetan con el nombre de propiedades.
Aprende a registrar un perfil con el generador de perfiles de montón. Algunos de los aspectos llamativos que podemos ver en el registro del generador de perfiles de montón a continuación incluyen la distancia desde la raíz del GC. Si casi todos los objetos del mismo tipo están a la misma distancia, y algunos están a mayor distancia, vale la pena investigar eso.
Dominadores
Los objetos dominadores constan de una estructura de árbol porque cada objeto tiene exactamente un dominador. Es posible que el dominador de un objeto no tenga referencias directas a un objeto que domina, es decir, el árbol del dominador no es un árbol de expansión del gráfico.
En el siguiente diagrama:
- El nodo 1 domina al 2.
- El nodo 2 domina al 3, al 4 y al 6.
- El nodo 3 domina al 5.
- El nodo 5 domina al 8.
- El nodo 6 domina al 7.
En el siguiente ejemplo, el nodo #3
es el dominador de #10
, pero #7
también existe en cada ruta de acceso simple de la recolección de elementos no utilizados a #10
. Por lo tanto, un objeto B es un dominador de un objeto A si B existe en cada ruta simple desde la raíz hasta el objeto A.
Detalles específicos de V8
Cuando se perfila una memoria, es útil comprender por qué las instantáneas de montón se ven de cierta manera. En esta sección, se describen algunos temas relacionados con la memoria que corresponden específicamente a la máquina virtual de JavaScript V8 (VM o VM de V8).
Representación de objetos de JavaScript
Existen tres tipos primitivos:
- Números (p.ej., 3.14159..).
- Booleanos (verdadero o falso)
- Cadenas (p.ej., "Werner Heisenberg")
No pueden hacer referencia a otros valores y siempre son hojas o nodos finales.
Los números se pueden almacenar de las siguientes maneras:
- Un valor entero inmediato de 31 bits denominado número entero pequeño (SMI)
- Objetos de montón, a los que se hace referencia como números de montón Los números de montón se usan para almacenar valores que no se ajustan al formulario de SMI, como dobles, o cuando un valor se debe encuadrar, como cuando se configuran propiedades en él.
Las cadenas se pueden almacenar de las siguientes maneras:
- El montón de VM
- externamente en la memoria del renderizador. Se crea un objeto wrapper que se usa para acceder a almacenamiento externo en el que, por ejemplo, se almacenan fuentes de secuencias de comandos y otro contenido que se recibe de la Web, en lugar de copiarse en el montón de la VM.
La memoria para los objetos de JavaScript nuevos se asigna desde un montón de JavaScript dedicado (o montón de VM). El recolector de elementos no utilizados de V8 administra estos objetos y, por lo tanto, permanecerán activos mientras haya al menos una referencia sólida a ellos.
Los objetos nativos son todo lo demás que no está en el montón de JavaScript. El recolector de elementos no utilizados V8 no administra el objeto nativo, a diferencia del objeto de montón, durante su vida útil, y solo se puede acceder a él desde JavaScript con su objeto wrapper de JavaScript.
Cons string es un objeto que consta de pares de strings almacenadas y luego unidas, y es el resultado de una concatenación. La unión del contenido del cons string solo se produce cuando es necesario. Un ejemplo sería cuando se debe construir una subcadena de una cadena unida.
Por ejemplo, si concatenas a y b, obtienes una string (a, b) que representa el resultado de la concatenación. Si luego concatenaste d con ese resultado, obtendrás otra string cons ((a, b), d).
Arreglos: Un array es un objeto con claves numéricas. Se usan ampliamente en la VM V8 para almacenar grandes cantidades de datos. Los arrays crean copias de seguridad de los conjuntos de pares clave-valor que se usan como diccionarios.
Un objeto típico de JavaScript puede ser uno de los dos tipos de array que se usan para almacenar:
- propiedades con nombre
- elementos numéricos
En los casos en los que hay una cantidad muy pequeña de propiedades, se pueden almacenar de forma interna en el objeto de JavaScript.
Map: Es un objeto que describe el tipo de objeto y su diseño. Por ejemplo, los objetos map se usan a fin de describir jerarquías de objetos implícitas para lograr un acceso rápido a las propiedades.
Grupos de objetos
Cada grupo de objetos nativos consta de objetos que tienen referencias mutuas entre sí. Considera, por ejemplo, un subárbol del DOM en el que cada nodo tiene un vínculo a su elemento superior y lo vincula al siguiente elemento secundario y al siguiente, lo que forma un gráfico conectado. Ten en cuenta que los objetos nativos no están representados en el montón de JavaScript; por eso, no tienen tamaño. En cambio, se crean objetos wrapper.
Cada objeto wrapper contiene una referencia al objeto nativo correspondiente para redireccionar comandos hacia él. A su vez, un grupo de objetos contiene objetos wrapper. Sin embargo, esto no crea un ciclo que no se pueda recopilar, ya que el GC es suficientemente inteligente como para liberar grupos de objetos a los que ya no se hace referencia a los wrappers. Sin embargo, si olvidas lanzar un solo wrapper, se mantendrá todo el grupo y los wrappers asociados.