Cómo entender fugas de memoria en las aplicaciones JavaScript

Detectar y abordar problemas de memoria

La recolección de basura puede ser liberadora. Nos permite concentrar en la lógica de las aplicaciones y no en la gestión de memorias. Sin embargo, la recolección de basura no es mágica. Entender cómo funciona y cómo se puede mantener la memoria mucho tiempo después de haber sido lanzada, trae como resultado aplicaciones más rápidas y confiables. En este artículo puede obtener información acerca de un enfoque sistemático para localizar fugas de memoria en aplicaciones JavaScript, sobre varios patrones de fuga comunes y sobre los métodos apropiados para solucionar dichas fugas.

Ben Dolmar, Desarrollador de software, The Nerdery

Ben DolmarProgramador profesional desde el 2001, Ben Dolmar cuenta con una amplia experiencia en tecnología, incluidos los programas ActionScript, iOS, JavaScript, PHP y Ruby, así como el diseño gráfico. Luego de graduarse con una doble especialización en periodismo y ciencias políticas de la Universidad de Wisconsin, Madison, en 1997, Ben fue director de producción en Faith Inkubators desde 1998 hasta el 2007. Desde que se unió a The Nerdery en el 2007, ha colaborado con el lanzamiento de más de 400 proyectos. En el 2012, Ben fue promovido a ingeniero jefe de software. Fue orador sobre ActionScript en SWFCamp y estuvo en SocialMediaDev Camp en Chicago, con una presentación sobre la evolución de los estándares web y sobre HTML5.



28-01-2013

Introducción

Cuando se manejan lenguajes de programación como JavaScript, es fácil olvidar que cada objeto, clase, cadena, número y método requiere que la memoria sea asignada y retenida. El lenguaje y el recopilador de basura del tiempo de ejecución ocultan los detalles de esa asignación y de su desasignación.

Es posible alcanzar muchos objetivos sin siquiera considerar la gestión de memorias, pero ignorarla puede causar problemas significativos en el programa. Los objetos mejorados de manera inadecuada pueden continuar mucho más tiempo que el previsto. Dichos objetos continúan ofreciendo respuestas a acontecimientos y consumiendo recursos. Pueden hacer que el navegador memorice la página desde una unidad de disco virtual y pueden ralentizar la computadora significativamente (y, en casos extremos, colapsar el navegador).

Se considera una fuga de memoria a cualquier objeto que perdura luego de que no se lo utiliza o necesita. En los últimos años, muchos navegadores han mejorado en relación a la recuperación de la memoria de JavaScript entre las cargas de una página. Sin embargo, no todos los navegadores se comportan de la misma manera. Tanto Firefox como Internet Explorer tienen un historial de fugas de memoria que continuaban hasta que el navegador se cerraba.

Muchos patrones típicos que históricamente provocaban fugas de memoria ya no están presentes en navegadores modernos. Sin embargo, actualmente existe una tendencia diferente que afecta la fuga de memoria. Actualmente, muchas personas diseñan aplicaciones web para que se ejecuten dentro del contexto de una sola página sin actualizaciones de página. En ese contexto, es fácil retener la memoria de un estado de la aplicación a otro cuando ya no se necesita o no es relevante.

En este artículo, obtenga más información acerca del ciclo de vida básico de un objeto, sobre cómo la recolección de basura determina si un objeto puede ser liberado o no, y sobre cómo evaluar comportamientos potenciales de fuga. Además, obtenga información sobre cómo utilizar Heap Profiler en Google Chrome para diagnosticar problemas en la memoria. Los ejemplos ilustran cómo abordar fugas de memoria con cierres, el registro de consola y ciclos.

Es posible descargar el código de origen para los ejemplos que se utilizan en este artículo.


Ciclo de vida del objeto

Para comprender la prevención de fugas de memoria, es importante comprender el ciclo de vida básico de un objeto. Cuando se crea un proyecto, JavaScript automáticamente asigna la cantidad de memoria apropiada para ese objeto. Desde ese momento, el recopilador de basura evalúa este objeto de manera continua para determinar si todavía es un objeto válido.

A intervalos regulares, el recopilador de basura realiza un barrido de la gráfica del objeto y calcula la cantidad de otros objetos que tienen una referencia a otro objeto. Si un objeto tiene un conteo de cero (ningún otro objeto tiene referencia a este) o si las únicas referencias son circulares, la memoria de los objetos puede ser recuperada. La Figura 1 muestra un ejemplo de cómo un recopilador de basura recupera la memoria.

Figura 1. Recuperación de memoria con la recolección de basura
4 steps showing the root node associating with various objects.

Sería útil poder ver el sistema realmente en acción, pero las herramientas para hacerlo son limitadas. Una manera de tener una idea de cuánta memoria consume su aplicación JavaScript es utilizar las herramientas del sistema para observar la asignación de memoria del navegador. Existen varias herramientas disponibles que le indicarán el nivel de uso actual y graficarán el uso de memoria de un proceso con el transcurso del tiempo.

Por ejemplo, si instaló XCode en Mac OSX puede lanzar la aplicación Instrumentos y adjuntar su herramienta de supervisión de actividad al navegador para obtener un análisis en tiempo real. En Windows®, puede utilizar el Gestor de tareas. Si observa que mientras se desplaza por la aplicación la gráfica del uso de memoria aumenta de manera consistente en el transcurso del tiempo, sabrá que existe una fuga de memoria.

La observación de la huella de memoria del navegador es un proxy preliminar para el uso real de la memoria de su aplicación JavaScript. Los datos del navegador no indican qué objetos presentan una fuga y no existe una garantía de que los datos realmente coinciden con la verdadera huella de su aplicación. Además, debido a problemas de implementación en algunos navegadores, es posible que los elementos DOM (u objetos de respaldo a nivel de aplicación) no sean liberados cuando el elemento correspondiente sea destruido en la página. Esto es especialmente cierto en el caso de una etiqueta de video, que requiere una infraestructura más elaborada para que el navegador la implemente.

Hubo varios intentos de añadir seguimiento a la asignación de memoria de bibliotecas JavaScript del cliente. Lamentablemente, ninguno de esos intentos ha sido especialmente confiable. Por ejemplo, el conocido paquete stats.js perdió apoyo debido a su inexactitud. Generalmente, intentar mantener o determinar esta información del cliente trae problemas porque presenta sobrecargas en la aplicación y no puede determinarse de manera fiable.

La solución ideal es que los proveedores de navegadores brinden una serie de herramientas en el navegador que ayuden a supervisar el uso de memoria, identificar objetos con fuga y determinar por qué un objeto determinado continúa marcado para retención.

Actualmente, el único navegador que puede implementar una herramienta de gestión de memoria como parte de sus herramientas de desarrollo es Google Chrome, que ofrece Heap Profiler. En este artículo se utiliza el Heap Profiler para probar e ilustrar cómo el tiempo de ejecución JavaScript maneja la memoria.


Análisis de las instantáneas de almacenamiento dinámico

Antes de crear una fuga de memoria, observe la interacción simple donde la memoria se recolecta de manera adecuada. Comience por crear una página HTML simple, con dos botones, como en el Listado 1.

Listado 1. index.html
<html>
<head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" 
type="text/javascript"></script>
</head>
<body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
    <script src="assets/scripts/leaker.js" type="text/javascript" 
charset="utf-8"></script>
    <script src="assets/scripts/main.js" type="text/javascript" 
charset="utf-8"></script>
</body>
</html>

Se incluyó jQuery para garantizar una sintaxis simple para el manejo de eventos enlazados que funcione correctamente en los navegadores y que ubique en paralelo las prácticas de desarrollo más comunes. Se añadieron etiquetas de secuencia para la clase leaker y para el método JavaScript principal. Durante la producción, generalmente esta es una mejor manera para unir sus archivos JavaScript en un solo archivo. A efectos de este ejemplo, es más fácil mantener la lógica en archivos separados.

Puede filtrar Heap Profiler para mostrar únicamente instancias de clases particulares. Para aprovechar esa función, puede crear una nueva clase que abarque el comportamiento del objeto con fuga y que pueda encontrarse fácilmente en el analizador, como en el Listado 2.

Listado 2. assets/scripts/leaker.js
var Leaker = function(){};
Leaker.prototype = {
    init:function(){

    }    
};

Asocie el botón de inicio para inicializar un objeto Leaker y asignarlo a una variable del espacio de nombres global. También deberá asociar el botón Destroy con un método que deberá mejorar el objeto Leaker y dejarlo listo para la recolección de basura, como en el Listado 3.

Listado 3. assets/scripts/main.js
$("#start_button").click(function(){
    if(leak !== null || leak !== undefined){
        return;
    }
  leak = new Leaker();
  leak.init();
});

$("#destroy_button").click(function(){
    leak = null;
});

var leak = new Leaker();

En este punto, ya puede crear un objeto, observarlo en la memoria y, luego, liberarlo.

  1. Cargar la página inicial en Chrome.

    Como jQuery se carga directamente desde Google, es necesaria una conexión a Internet para ejecutar la muestra.

  2. Abra las herramientas de desarrollo, para esto abra el menú View y seleccione el submenú Develop. Seleccione el comando Developer Tools .
  3. Diríjase a la etiqueta Profiles y tome una instantánea de almacenamiento dinámico, como se indica en la Figura 2.
    Figura 2. Etiqueta de perfiles
    Screen shot of the profiles tab on Google Chrome.
  4. Vuelva a la página web y seleccione Start.
  5. Tome otra instantánea de almacenamiento dinámico.
  6. Filtre la primera instantánea, mediante la búsqueda de la clase Leaker . No debería encontrar ninguna instancia de fuga. Pase a la segunda instantánea y debería hallar una sola instancia, como en la Figura 3.
    Figura 3. Instancia de instantánea
    Screen shot of the Heap Profiler filter page
  7. Vuelva a la página web y seleccione Destroy
  8. Tome una tercera instantánea de almacenamiento dinámico.
  9. Filtre la tercera instantánea, mediante la búsqueda de la clase Leaker . No debería encontrar ninguna instancia de fuga.

    Como otra alternativa, y con la tercera instantánea cargada, cambie los modos de análisis de Resumen a Comparación, y coteje la tercera instantánea con la segunda. Debería observar una diferencia de -1 (una instancia del objeto Leaker fue liberada entre ambas instantáneas).

¡Hurra! La recolección de basura comienza. Ahora es momento de interrumpirla.


Fuga de memoria 1: Cierres

Una manera fácil de evitar que un objeto sea recogido con la basura es contar con un intervalo o tiempo de ejecución que haga referencia al objeto en su devolución de llamada. Para ver esta acción, actualice la clase leaker.js, como en el Listado 4.

Listado 4. assets/scripts/leaker.js
var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        this._interval = null;
        this.start();
    },

    start: function(){
        var self = this;
        this._interval = setInterval(function(){
            self.onInterval();
        }, 100);
    },

    destroy: function(){
        if(this._interval !== null){
            clearInterval(this._interval);          
        }
    },

    onInterval: function(){
        console.log("Interval");
    }
};

Ahora, cuando repita los pasos del 1 al 9 en la sección anterior, debería ver que en la tercera instantánea el objeto Leaker continúa presente y que el intervalo sigue ejecutándose indefinidamente. Entonces, ¿qué sucedió? Cualquier variable local a la que se haga referencia en un cierre es retenida por el cierre tanto tiempo como dure el cierre. Para garantizar que la devolución de llamada para el método setInterval ejecutado con acceso al alcance de la instancia de fuga, esta variable se asignó a la variable local self, que se utilizó para desencadenar onInterval desde el cierre. Cuando onInterval se activa, tiene acceso a cualquier variable de instancia en el objeto Leaker , incluido self. Sin embargo, el objeto Leaker no es recogido con la basura mientras exista el receptor de eventos.

Para solucionar el problema, desencadene el método destroy que fue añadido al objeto leaker antes de anular la referencia almacenada, actualizando el gestor de clics para el botón de destrucción, como en el Listado 5.

Listado 5. assets/scripts/main.js
$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

Destrucción de objetos y propiedad de objetos

Es recomendable contar con un método estándar para hacer que un objeto sea elegible para la recolección de basura. El principal propósito de la función de destrucción es centralizar la responsabilidad de mejorar cualquier acción que el objeto haya ejecutado que:

  • Evite que el recuento de referencia descienda a 0 (por ejemplo, quitar receptores de evento problemáticos y devoluciones de llamadas, y anular el registro de cualquier servicio).
  • Consuma ciclos de CPU innecesarios, como intervalos o animaciones.

El método destroy es generalmente un paso necesario para la mejora de un objeto, para raramente es suficiente. Otros objetos que conserven una referencia al objeto destruido podrían, en teoría, utilizar métodos en este objeto luego de que la instancia haya sido destruida. Como los resultados de esta situación son impredecibles, es fundamental que el método de destrucción sea utilizado solo cuando el objeto realmente vaya a eliminarse.

Generalmente, los métodos de destrucción son mejores cuando hay un propietario definido para un objeto responsable de su ciclo de vida. Esta situación suele ocurrir en sistemas jerárquicos, como vistas y controladores en marcos de referencia MVC o gráficas de escena para un sistema de renderizado en espacio.


Fuga de memoria 2: Registro de consola

Registrar un objeto en la consola es una manera particularmente difícil para retener un objeto en la memoria. El Listado 6 actualiza la clase Leaker para mostrar un ejemplo de esto.

Listado 6. assets/scripts/leaker.js
var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        console.log("Leaking an object: %o", this);
    },

    destroy: function(){

    }      
};

Puede demostrar el efecto de la consola mediante los siguientes pasos.

  1. Cargue la página inicial.
  2. Haga clic en Start.
  3. Diríjase a la consola y verifique que el objeto de fuga haya sido rastreado.
  4. Haga clic en Destroy.
  5. Regrese a la consola y escriba leak para registrar los contenidos actuales de la variable global. En este punto, el valor debe ser nulo.
  6. Tome otra instantánea de almacenamiento dinámico y realice un filtro para el objeto de fuga.

    Debe quedar una Leaker .

  7. Regrese a la consola y al estado original.
  8. Tome un perfil de almacenamiento dinámico más.

    La fuga restante debería haber sido mejorada antes de que la consola regrese a su estado original.

El registro de la consola en todo el perfil de la memoria podría causar un problema potencial significativo que muchos desarrolladores ni siquiera consideran. Registrar el objeto equivocado puede mantener grandes bloques de datos en la memoria. Es importante tener en cuenta que esto también se aplica a lo siguiente:

  • Objetos que se registran durante una sesión interactiva en la consola en la que el usuario escribe en JavaScript.
  • Objetos que son registrados por los métodos console.log y console.dir .

Fuga de memoria 3: Ciclos

Un ciclo tiene lugar cuando dos objetos hacen referencia uno al otro, de tal manera que ambos retienen al otro, como en la Figura 4.

Figura 4. Referencias que crean un ciclo
Figure with a blue root node connecting to two green boxes that show a connection between them

El Listado 7 muestra un ejemplo de código simple.

Listado 7. assets/scripts/leaker.js
var Leaker = function(){};

Leaker.prototype = {
    init:function(name, parent){
        this._name = name;
        this._parent = parent;
        this._child = null;
        this.createChildren();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create a child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this);
    },

    destroy: function(){

    }
};

La ejemplificación del objeto raíz se modificaría, como en el Listado 8.

Listado 8. assets/scripts/main.js
leak = new Leaker(); 
leak.init("leaker 1", null);

Si ejecuta un análisis de almacenamiento dinámico luego de crear y destruir los objetos, debe observar que el detector de basura identificó la referencia circular y liberó la memoria cuando seleccionó el botón de destrucción.

Sin embargo, si se introduce un tercer objeto que dificulta el proceso child, el ciclo provoca una fuga de memoria. Por ejemplo, cree un objeto registry , como en el Listado 9:

Listado 9. assets/scripts/leaker.js
var Registry = function(){};

Registry.prototype = {
    init:function(){
        this._subscribers = [];
    },

    add:function(subscriber){
        if(this._subscribers.indexOf(subscriber) >= 0){
            // Already registered so bail out
            return;
        }
        this._subscribers.push(subscriber);
    },

    remove:function(subscriber){
        if(this._subscribers.indexOf(subscriber) < 0){
            // Not currently registered so bail out
            return;
        }
              this._subscribers.splice(
                  this._subscribers.indexOf(subscriber), 1
              );
    }
};

La clase registry es un ejemplo simple de un objeto que permite que otras clases se registren con este y que luego sean eliminadas del registro. Si bien esta clase particular no hace algo con el registro, este es un patrón común en planificadores de eventos y sistemas de notificación.

Importar esa clase a la página index.html antes de leaker.js, como en el Listado 10.

Listado 10. index.html
<script src="assets/scripts/registry.js" type="text/javascript" 
charset="utf-8"></script>

Actualice el objeto Leaker para registrarlo con el objeto registry (aparentemente para la notificación sobre algunos eventos pendientes de aplicación). Esto genera una ruta alternativa del nodo raíz para que child leaker sea retenida y porque debido al ciclo, parent leaker también será retenida, como en el Listado 11.

Listado 11. assets/scripts/leaker.js
var Leaker = function(){};
Leaker.prototype = {

    init:function(name, parent, registry){
        this._name = name;
        this._registry = registry;
        this._parent = parent;
        this._child = null;
        this.createChildren();
        this.registerCallback();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this, this._registry);
    },

    registerCallback:function(){
        this._registry.add(this);
    },

    destroy: function(){
        this._registry.remove(this);
    }
};

Finalmente, actualice main.js para configurar el registro y llevar una referencia al registro al objeto parent leaker , como en el Listado 12:

Listado 12. assets/scripts/main.js
	  $("#start_button").click(function(){
  var leakExists = !(
	      window["leak"] === null || window["leak"] === undefined
	  );
  if(leakExists){
      return;
  }
  leak = new Leaker();
  leak.init("leaker 1", null, registry);
});

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

registry = new Registry();
registry.init();

Ahora, cuando ejecute un análisis de almacenamiento dinámico, debe observar que cada vez que selecciona el botón para comenzar, se crea y retiene dos instancias nuevas del objeto Leaker . La Figura 5 muestra el flujo de las referencias del objeto.

Figura 5. Fuga de memoria causada por referencias retenidas
3 boxes showing three various paths between the root node and the parent and children

A primera vista, puede parecer que es un ejemplo forzado, pero de hecho es bastante común. En marcos de referencia típicamente orientados al objeto, los receptores de eventos frecuentemente siguen patrones como los de la Figura 5. Este tipo de patrón también puede encajar con problemas causados por cierres y registros de consola.

A pesar de que existen diversas maneras de abordar este tipo de problema, en este caso el cambio más sencillo es actualizar la clase Leaker para destruir sus objetos children cuando sea destruida. Para el ejemplo, actualizar el método destroy , como en el Listado 13, sería suficiente.

Listado 13. assets/scripts/leaker.js
destroy: function(){
    if(this._child !== null){
        this._child.destroy();            
    }
    this._registry.remove(this);
}

Algunas veces, existe un ciclo entre dos objetos que no tienen una relación lo suficientemente fuerte para que cada uno asuma la responsabilidad del ciclo de vida del otro objeto. En tal caso, el objeto que estableció la relación con los dos objetos debe asumir la responsabilidad de romper el ciclo cuando sea destruido.


Conclusión

Aunque la basura en JavaScript sean recolectada, existen muchas maneras de retener objetos no deseados en la memoria. Muchos navegadores modernos han mejorado en cuanto a la limpieza de la memoria, pero las herramientas disponibles para el almacenamiento dinámico de la memoria de su aplicación son todavía limitadas—, a excepción de Google Chrome. Si se comienza con casos de prueba simple, es bastante sencillo evaluar comportamientos de fuga potenciales y determinar si existe una fuga.

Es imposible medir el uso de la memoria de manera exacta sin una prueba. Es muy sencillo permitir que referencias circulares conserven grandes porciones de la gráfica de objeto. Heap Profiler de Chrome es una herramienta valiosa para el diagnóstico de problemas de memoria; es una buena idea utilizarla de manera regular mientras efectúa desarrollos. Conserve expectativas concretas para cuando espera que se liberen recursos específicos en la gráfica de objeto y, luego, verifíquelas. Cada vez que observe que un resultado no es el esperado, examínelo.

Planificar la limpieza de un objeto cuando crea el objeto es mucho más fácil que intentar injertar una etapa de limpieza en la aplicación más adelante. Siempre cuente con un plan para quitar cualquier receptor de eventos y detenga cualquier intervalo que usted cree. Esté al tanto del uso de la memoria en su aplicación y podrá obtener aplicaciones más confiables y con mejor funcionamiento.


Descargar

DescripciónNombretamaño
Article source codeJavascriptMemoryManagementSource.zip4KB

Recursos

Aprender

Obtener los productos y tecnologías

Comentar

  • Comunidad de developerWorks: Conéctese con otros usuarios de developerWorks mientras explora los blogs conducidos por desarrolladores, foros, grupos y wikis.

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=856289
ArticleTitle=Cómo entender fugas de memoria en las aplicaciones JavaScript
publish-date=01282013