En esta sección, se describen los términos comunes que se usan en el análisis de memoria y se aplica a una variedad de herramientas de generación de perfiles de memoria para diferentes lenguajes.
Los términos y conceptos 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, este puede ser un repaso.
Tamaños de los objetos
Piensa en la memoria como un grafo con tipos primitivos (como números y cadenas) y objetos (arrays asociativos). Se puede representar visualmente como un gráfico con una serie de puntos interconectados de la siguiente manera:
Un objeto puede contener memoria de dos maneras:
- Directamente por el objeto
- De forma implícita, a través de la retención de referencias a otros objetos y, por lo tanto, evitando que un recolector de basura (GC) los elimine automáticamente.
Cuando trabajes con el Generador de perfiles de montón en DevTools (una herramienta para investigar problemas de memoria que se encuentran en el panel Memoria), es probable que veas varias columnas de información diferentes. Dos que se destacan son Shallow Size y Retained Size, pero ¿qué representan?
Tamaño aplanado
Es el tamaño de la memoria que contiene el objeto.
Los objetos JavaScript típicos tienen cierta memoria reservada para su descripción y para almacenar valores inmediatos. Por lo general, solo los arrays y las cadenas pueden tener un tamaño reducido significativo. Sin embargo, las cadenas y los arrays externos suelen tener su almacenamiento principal en la memoria del renderizador, lo que expone solo un pequeño objeto de wrapper en la pila 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 contener una gran cantidad de memoria de forma indirecta, ya que evita que el proceso de recolección de basura automática se deshaga de otros objetos.
Tamaño retenido
Es el tamaño de la memoria que se libera una vez que se borra el objeto junto con sus objetos dependientes que no se pudieron alcanzar desde las raíces de GC.
Las raíces de GC se componen de controles que se crean (ya sea locales o globales) cuando se hace una referencia desde el código nativo a un objeto JavaScript fuera de V8. Todos estos identificadores se pueden encontrar en una instantánea del montón en Raíces de GC > Alcance de identificadores y Raíces de GC > Identificadores globales. Describir los controladores en esta documentación sin entrar en detalles de la implementación del navegador puede ser confuso. No tienes que preocuparte por las raíces de GC ni por los controladores.
Hay muchas raíces internas de GC, 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 ventana (en cada iframe). Hay un campo de distancia en las instantáneas del 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 consta de todos los nodos DOM nativos a los que se puede acceder a través del documento. Es posible que no todos tengan wrappers de JS, pero si los tienen, estos estarán activos mientras el documento esté activo.
- A veces, el contexto del depurador y la consola de DevTools pueden retener objetos (p.ej., después de la evaluación de la consola). Crea instantáneas del montón con una consola clara y sin puntos de interrupción activos en el depurador.
El gráfico de 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 borra este objeto raíz.
Todo lo que no se pueda alcanzar desde la raíz obtiene GC.
Árbol de retención de objetos
El montón es una red de objetos interconectados. En el mundo matemático, esta estructura se denomina grafo o grafo de memoria. Un gráfico se construye a partir de nodos conectados por medio de aristas, a los que se les asignan etiquetas.
- Los nodos (o objetos) se etiquetan con el nombre de la función constructor que se usó para compilarlos.
- Los bordes se etiquetan con los nombres de las propiedades.
Obtén información para registrar un perfil con el Generador de perfiles de montón. Algunos de los aspectos llamativos que podemos ver en la siguiente grabación del generador de perfiles de montón incluyen la distancia: la distancia desde la raíz de GC. Si casi todos los objetos del mismo tipo están a la misma distancia y algunos están a una distancia mayor, vale la pena investigarlo.
Dominadores
Los objetos dominadores se componen de una estructura de árbol porque cada objeto tiene exactamente un dominador. Es posible que un 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 diagrama que se muestra a continuación, se ilustra lo siguiente:
- El nodo 1 domina el nodo 2
- El nodo 2 domina los nodos 3, 4 y 6.
- El nodo 3 domina el nodo 5
- El nodo 5 domina el nodo 8
- El nodo 6 domina el nodo 7
En el siguiente ejemplo, el nodo #3
es el dominante de #10
, pero #7
también existe en cada ruta simple de GC 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 de V8
Cuando se genera un perfil de memoria, es útil comprender por qué las instantáneas del montón se ven de una manera determinada. 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 nodos finales o hojas.
Los números se pueden almacenar de las siguientes maneras:
- un valor entero inmediato de 31 bits llamado número entero pequeño (SMIs)
- objetos del montón, denominados números de montón. Los números de montón se usan para almacenar valores que no se ajustan al formato SMI, como los dobles, o cuando un valor debe estar enmarcado, como cuando se configuran propiedades en él.
Las cadenas se pueden almacenar en cualquiera de los siguientes lugares:
- el montón de VM
- de forma externa en la memoria del renderizador. Se crea y usa un objeto wrapper para acceder al almacenamiento externo, en el que, por ejemplo, se almacenan las 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 JavaScript nuevos se asigna desde una pila de JavaScript dedicada (o pila de VM). El recolector de basura de V8 administra estos objetos y, por lo tanto, permanecerán activos mientras haya al menos una referencia fuerte a ellos.
Los objetos nativos son todo lo que no está en el montón de JavaScript. A diferencia del objeto del montón, el recolector de basura V8 no administra el objeto nativo durante su ciclo de vida, y solo se puede acceder a él desde JavaScript con su objeto de wrapper de JavaScript.
La cadena de cons es un objeto que consta de pares de cadenas almacenadas y, luego, unidas, y es un resultado de la concatenación. La unión del contenido de la cadena de cons solo se produce según sea necesario. Un ejemplo sería cuando se debe construir una subcadena de una cadena unida.
Por ejemplo, si concatenas a y b, obtienes una cadena (a, b) que representa el resultado de la concatenación. Si más adelante concatenas d con ese resultado, obtendrás otra cadena cons ((a, b), d).
Arrays: Un array es un objeto con claves numéricas. Se usan mucho en la VM de V8 para almacenar grandes cantidades de datos. Los conjuntos de pares clave-valor que se usan como diccionarios crean copias de seguridad con آرایهها.
Un objeto JavaScript típico puede ser uno de los dos tipos de array que se usan para almacenar lo siguiente:
- propiedades con nombre
- elementos numéricos
En los casos en 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 mapas se usan para describir jerarquías de objetos implícitas para el acceso rápido a propiedades.
Grupos de objetos
Cada grupo de objetos nativos está formado por objetos que contienen referencias mutuas entre sí. Considera, por ejemplo, un subárbol de DOM en el que cada nodo tiene un vínculo a su elemento superior y vínculos al siguiente elemento secundario y al siguiente hermano, lo que forma un gráfico conectado. Ten en cuenta que los objetos nativos no se representan en el montón de JavaScript, por lo que tienen un tamaño de cero. En su lugar, se crean objetos wrapper.
Cada objeto wrapper contiene una referencia al objeto nativo correspondiente para redireccionar los comandos a él. A su vez, un grupo de objetos contiene objetos de wrapper. Sin embargo, esto no crea un ciclo no recuperable, ya que el GC es lo suficientemente inteligente como para liberar grupos de objetos a cuyos wrappers ya no se hace referencia. Sin embargo, si olvidas liberar un solo wrapper, se detendrá todo el grupo y los wrappers asociados.