En esta página

Gestión de memoria en el motor JavaScript

Introducción

Este documento describe la gestión dinámica de memoria del Motor JavaScript en QML. Es una descripción bastante técnica y profunda. Sólo necesita leerlo si le interesan las características exactas de la gestión de memoria de JavaScript en QML. En particular, puede ser útil si estás intentando optimizar tu aplicación para obtener el máximo rendimiento.

Nota: Al compilar tu código QML a C++ utilizando el compiladorQt Quick puedes evitar gran parte del uso del heap de JavaScript. El código C++ generado utiliza la pila y el montón conocidos de C++ para almacenar objetos y valores. El entorno de host JavaScript, sin embargo, siempre utiliza algo de memoria gestionada por JavaScript, independientemente de si la utilizas o no. No obstante, si se utilizan funciones que no se pueden compilar en C++, el motor recurrirá a la interpretación o a la compilación JIT y utilizará objetos de JavaScript almacenados en el montón de JavaScript.

Principios básicos

El motor JavaScript de QML dispone de un gestor de memoria dedicado que solicita al sistema operativo espacio de direcciones en unidades de varias páginas. Los objetos, cadenas y otros valores gestionados creados en JavaScript se colocan entonces en este espacio de direcciones, utilizando el esquema de asignación propio del motor JavaScript. El motor JavaScript no utiliza las funciones malloc() y free() de la biblioteca C, ni las implementaciones por defecto de new y delete de C++ para asignar memoria a los objetos JavaScript.

Las peticiones de espacio de direcciones se hacen generalmente con mmap() en sistemas tipo Unix y con VirtualAlloc() en Windows. Existen varias implementaciones de estas primitivas específicas para cada plataforma. El espacio de direcciones reservado de esta forma no se asigna inmediatamente a la memoria física. Más bien, el sistema operativo se da cuenta de cuándo se accede realmente a una página de memoria y sólo entonces la consigna. Por lo tanto, el espacio de direcciones está prácticamente libre y disponer de una gran cantidad de él proporciona al gestor de memoria de JavaScript la ventaja que necesita para colocar objetos de forma eficiente en la pila de JavaScript. Además, existen técnicas específicas de la plataforma para indicar al sistema operativo que un trozo de espacio de direcciones, aunque siga reservado, no tiene que asignarse a la memoria física por el momento. El sistema operativo puede entonces liberar la memoria cuando lo necesite y utilizarla para otras tareas. Lo más importante es que la mayoría de los sistemas operativos no garantizan una acción inmediata ante una solicitud de descompromiso. Sólo liberarán la memoria cuando sea realmente necesaria para otra cosa. En los sistemas tipo Unix se suele utilizar madvise() para ello. Windows tiene banderas específicas para VirtualFree() para hacer el equivalente.

Nota: Hay herramientas de perfilado de memoria que no entienden este mecanismo y sobreinforman del uso de memoria de JavaScript.

Todos los valores almacenados en la pila de JavaScript están sujetos a la recolección de basura. Ninguno de los valores se "borra" inmediatamente cuando salen del ámbito o se "eliminan" de cualquier otra forma. Sólo el recolector de basura puede eliminar valores del montón de JavaScript y devolver memoria (véase Recolección de basura más abajo para saber cómo funciona).

Tipos basados en QObject

QObject-Los tipos basados en QObject, y en particular todo lo que se puede frasear como un elemento QML, se asignan en la pila de C++. Sólo se coloca una pequeña envoltura alrededor del puntero en la pila de JavaScript cuando se accede a QObject desde JavaScript. Sin embargo, dicha envoltura puede ser propietaria del QObject al que apunta. Véase QJSEngine::ObjectOwnership. Si la envoltura es propietaria del objeto, éste se eliminará cuando se recoja la basura de la envoltura. También puedes activar manualmente la eliminación llamando al método destroy(). destroy() llama internamente a QObject::deleteLater(). Por tanto, no borrará inmediatamente el objeto, sino que esperará a la siguiente iteración del bucle de eventos.

Las propiedades de objetos declaradas en QML se almacenan en el montón de JavaScript. Viven mientras vive el objeto al que pertenecen. Después se eliminan la próxima vez que se ejecuta el recolector de basura.

Asignación de objetos

En JavaScript, cualquier tipo estructurado es un objeto. Esto incluye objetos de función, matrices, expresiones regulares, objetos de fecha y mucho más. QML dispone de una serie de tipos de objeto internos, como el mencionado QObject wrapper. Cada vez que se crea un objeto, el gestor de memoria lo almacena en el montón de JavaScript.

Las cadenas de JavaScript también son valores gestionados, pero sus datos de cadena no se asignan en el montón de JavaScript. Al igual que las envolturas de QObject, los objetos de heap para cadenas son sólo envolturas finas alrededor de un puntero a datos de cadena.

Cuando se asigna memoria a un objeto, el tamaño del objeto se redondea primero a una alineación de 32 bytes. Cada trozo de espacio de direcciones de 32 bytes se denomina "slot". Para objetos más pequeños que un umbral de "tamaño enorme", el gestor de memoria realiza una serie de intentos para colocar el objeto en la memoria:

  • El gestor de memoria mantiene listas enlazadas de trozos de montón liberados previamente, denominadas "ubicaciones". Cada contenedor contiene trozos de montón con un tamaño fijo por contenedor en ranuras. Si la bandeja del tamaño adecuado no está vacía, elige la primera entrada y coloca el objeto en ella.
  • La memoria que aún no se ha utilizado se gestiona mediante un asignador de parachoques. Un puntero bumper apunta al byte más allá del espacio de direcciones ocupado. Si todavía hay suficiente espacio de direcciones sin utilizar, el parachoques se incrementa en consecuencia, y el objeto se coloca en el espacio no utilizado.
  • Se mantiene una papelera separada para los trozos de heap liberados previamente, de diferentes tamaños, mayores que los tamaños específicos mencionados anteriormente. El gestor de memoria recorre esta lista e intenta encontrar un trozo que pueda dividir para acomodar el nuevo objeto.
  • El gestor de memoria busca en las listas de trozos de tamaño específico mayores que el objeto que se va a asignar e intenta dividir uno de ellos.
  • Finalmente, si nada de lo anterior funciona, el gestor de memoria reserva más espacio de direcciones y asigna el objeto utilizando el asignador de parachoques.

Los objetos enormes son tratados por su propio asignador. Para cada uno de ellos se obtienen una o más páginas de memoria separadas del SO y se gestionan por separado.

Además, cada nuevo trozo de espacio de direcciones que el gestor de memoria obtiene del SO recibe una cabecera que contiene una serie de banderas para cada ranura:

  • objeto: La primera ranura ocupada por un objeto se marca con este bit.
  • extiende: Cualquier otro espacio ocupado por un objeto se marca con este bit.
  • mark: Cuando el recolector de basura se ejecuta, activa este bit si el objeto todavía está en uso.

Clases internas

Para minimizar el almacenamiento necesario de metadatos sobre los miembros de un objeto, el motor JavaScript asigna una "clase interna" a cada objeto. Otros motores JavaScript llaman a esto "clase oculta" o "forma". Las clases internas se deduplican y se guardan en un árbol. Si se añade una propiedad a un objeto, se comprueban los hijos de la clase interna actual para ver si se ha producido antes la misma disposición del objeto. Si es así, podemos utilizar la clase interna resultante inmediatamente. En caso contrario, hay que crear una nueva.

Las clases internas se almacenan en su propia sección del montón de JavaScript que, por lo demás, funciona del mismo modo que la asignación general de objetos descrita anteriormente. Esto se debe a que las clases internas deben mantenerse vivas mientras se recopilan los objetos que las utilizan. Las clases internas se recopilan en una pasada separada.

Sin embargo, los atributos de propiedad almacenados en las clases internas no se guardan en el montón de JavaScript, sino que se gestionan mediante new y delete.

Recogida de basura

El recolector de basura utilizado en el motor JavaScript es un diseño sin movimiento, Mark and Sweep. Desde Qt 6.8, se ejecuta incrementalmente por defecto (a menos que QV4_GC_TIMELIMIT esté a 0). En la fase de marcado recorremos todos los lugares conocidos donde se pueden encontrar referencias vivas a objetos. En concreto

  • Globales de JavaScript
  • Partes indelebles de QML y unidades de compilación de JavaScript
  • La pila de JavaScript
  • El almacenamiento de valores persistentes. Aquí es donde QJSValue y clases similares guardan referencias a objetos JavaScript.

Para cualquier objeto que se encuentre en esos lugares, los bits de marca se establecen de forma recursiva para cualquier cosa a la que haga referencia.

En la fase de barrido, el recolector de basura recorre todo el montón y libera todos los objetos que no se hayan marcado antes. La memoria liberada resultante se clasifica en contenedores que se utilizarán para nuevas asignaciones. Si un trozo de espacio de direcciones está completamente vacío, se libera, pero el espacio de direcciones se conserva (véase Principios básicos más arriba). Si el uso de memoria vuelve a crecer, se reutiliza el mismo espacio de direcciones.

El recolector de basura se activa manualmente llamando a la función gc() o mediante una heurística que tiene en cuenta los siguientes aspectos:

  • La cantidad de memoria gestionada por objeto en el heap de JavaScript, pero no asignada directamente en el heap de JavaScript, como cadenas y datos de miembros internos de la clase. Para ellos se mantiene un umbral dinámico. Si se supera, se ejecuta el recolector de basura y se incrementa el umbral. Si la cantidad de memoria externa gestionada cae muy por debajo del umbral, éste se reduce.
  • El espacio total de direcciones reservado. La asignación de memoria interna en el montón de JavaScript sólo se considera después de que se haya reservado al menos parte del espacio de direcciones.
  • La reserva adicional de espacio de direcciones desde la última ejecución del recolector de basura. Si la cantidad de espacio de direcciones es más del doble de la cantidad de memoria utilizada después de la última ejecución del recolector de basura, volvemos a ejecutar el recolector de basura.

Análisis del uso de la memoria

Para observar el desarrollo tanto del espacio de direcciones como del número de objetos asignados en él, lo mejor es utilizar una herramienta especializada. El sitio QML Profiler proporciona una visualización que ayuda aquí. Las herramientas más genéricas no pueden ver lo que hace el gestor de memoria de JavaScript dentro del espacio de direcciones que reserva y puede que ni siquiera se den cuenta de que parte del espacio de direcciones no está asignado a la memoria física.

Otra forma de depurar el uso de la memoria son las logging categories qt.qml.gc.statistics y qt.qml.gc.allocatorStats. Si habilitas el nivel Debug para qt.qml.gc.statistics, el recolector de basura imprimirá alguna información cada vez que se ejecute:

  • Cuánto espacio de direcciones total está reservado
  • Cuánta memoria estaba en uso antes y después de la recolección de basura
  • Cuántos objetos de distintos tamaños se han asignado hasta el momento

El nivel Debug de qt.qml.gc.allocatorStats imprime estadísticas más detalladas que también incluyen cómo se activó el recolector de basura, los tiempos de las fases de marcado y barrido y un desglose detallado del uso de memoria por bytes y trozos de espacio de direcciones.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.