Terminología de la memoria

Meggin Kearney
Meggin Kearney

En esta sección, se describen términos comunes que se usan en el análisis de memoria y se pueden aplicar a una variedad de herramientas de generación de perfiles de memoria para diferentes lenguajes.

Los términos y nociones que se describen aquí se refieren al Generador de perfiles de montón de Herramientas para desarrolladores de Chrome. Si ya trabajaste con Java, .NET o algún otro generador de perfiles de memoria, es posible que esto sea un repaso.

Tamaños de objetos

Piensa en la memoria como un gráfico con tipos primitivos (como números y strings) y objetos (arreglos asociativos). Se puede representar visualmente como un gráfico con varios puntos interconectados de la siguiente manera:

Representación visual de la memoria

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 un recolector de elementos no utilizados (GC) elimine esos objetos 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 varias columnas de información diferentes. Las dos opciones que se destacan son Shallow Size y Retained Size, pero ¿qué representan?

Tamaño superficial y retenido

Tamaño aplanado

Este es el tamaño de la memoria que retiene el objeto en sí.

Los objetos típicos de JavaScript tienen 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 suelen tener su almacenamiento principal en la memoria del procesador y solo exponen un objeto wrapper pequeño en el montón de JavaScript.

La memoria del procesador 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 inicia la página. Sin embargo, incluso un objeto pequeño puede retener una gran cantidad de memoria de forma indirecta, lo 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 el objeto en sí se borra junto con sus objetos dependientes a los que no se puede acceder desde las raíces de GC.

Las raíces de GC están compuestas por identificadores que se crean (ya sean locales o globales) cuando se hace una referencia de código nativo a un objeto de JavaScript fuera de V8. Todos esos controladores se pueden encontrar en una instantánea del montón en GC roots > Handle scope y GC roots > Global controls. 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 y los controladores.

Hay 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). Hay un campo de distancia en las instantáneas del montón, que es el número de referencias de propiedad en la ruta de retención más corta desde la ventana.
  • Árbol del DOM del documento que consta de todos los nodos nativos del DOM que se pueden alcanzar 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 los objetos (p.ej., después de la evaluación de la consola). Crea instantáneas de montón con una consola vacía 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 puedes controlar la forma en que se recolecta este objeto raíz.

No se puede controlar el objeto raíz

Lo que no es accesible 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 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 constructor que se usó para compilarlos.
  • Los bordes se etiquetan con los nombres de propiedades.

Obtén información sobre cómo grabar un perfil con el generador de perfiles del montón. Entre los aspectos más llamativos que podemos ver en el registro del generador de perfiles de montón, se incluye la distancia: la distancia desde la raíz de recolección de elementos no utilizados. 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.

Distancia desde la raíz

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 carezca de 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, se ilustra lo siguiente:

  • 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.

Estructura del árbol dominador

En el siguiente ejemplo, el nodo #3 es el dominador de #10, pero #7 también existe en todas las rutas de acceso simples de GC a #10. Por lo tanto, un objeto B es el dominador del objeto A si B existe en cada ruta de acceso simple desde la raíz hasta el objeto A.

Ilustración animada de dominador

Información específica de V8

Cuando generas perfiles de memoria, resulta útil comprender la razón por la que las instantáneas de montón se ven de cierta manera. En esta sección, se describen algunos temas relacionados con la memoria que se relacionan específicamente con la máquina virtual de JavaScript V8 (VM o VM 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úmeros enteros pequeños (SSI).
  • Objetos de montón, conocidos como números del montón. Los números del montón se usan para almacenar valores que no se ajustan al formato de SMI, como los dobles, o cuando un valor se debe encuadrar, por ejemplo, para establecer propiedades en él.

Las Strings se pueden almacenar de las siguientes maneras:

  • el montón de la VM
  • de forma externa en la memoria del procesador. Un objeto wrapper se crea y se usa para acceder al almacenamiento externo donde, por ejemplo, se almacenan las fuentes de secuencias de comandos y otro contenido que se recibe de la Web, en lugar de copiarlo en el montón de 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 fuerte a ellos.

Los objetos nativos son todo lo demás que no está en el montón de JavaScript. El objeto nativo, a diferencia del objeto del montón, no es administrado por el recolector de elementos no utilizados de V8 durante su vida útil, y solo se puede acceder a él desde JavaScript con el objeto wrapper de JavaScript.

Cons string es un objeto que consiste en pares de strings almacenadas y unidas, y es el resultado de una concatenación. La unión del contenido de cons string solo se produce cuando es necesario. Un ejemplo sería cuando se debe construir una substring de una string unida.

Por ejemplo, si concatenas a y b, obtienes una string (a, b), que representa el resultado de la concatenación. Si luego concatenas d con ese resultado, obtienes otra cons string ((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 dos tipos de array que se usan para almacenar:

  • propiedades con nombre y
  • elementos numéricos

Cuando hay una cantidad muy pequeña de propiedades, se pueden almacenar de manera 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 a fin de lograr un acceso rápido a las propiedades.

Grupos de objetos

Cada grupo de objetos nativos está compuesto por objetos que tienen referencias mutuas. Considera, por ejemplo, un subárbol del DOM en el que cada nodo tiene un vínculo a su elemento superior y al siguiente elemento secundario y secundario, 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 no tienen tamaño. En cambio, se crean objetos wrapper.

Cada objeto wrapper contiene una referencia al objeto nativo correspondiente para redireccionarle comandos. A su vez, un grupo de objetos contiene objetos wrapper. Sin embargo, esto no crea un ciclo que no se pueda recopilar, ya que la recolección de elementos no usados 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 conservarán todo el grupo y los wrappers asociados.