Cómo se recogen los cierres de JavaScript


He registrado el siguiente Error de Chrome , que ha llevado a muchas fugas de memoria graves y no obvias en mi código:

(Estos resultados utilizan Chrome Dev Tools' memory profiler, que ejecuta el GC, y luego toma una instantánea del montón de todo lo que no se recoge.)

En el siguiente código, la instancia someClass es basura recolectada (buena):

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

Pero no será basura recogida en este caso (mala):

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

Y el correspondiente captura de pantalla:

captura de pantalla de Chromebug

Parece que un cierre (en este caso, function() {}) mantiene todos los objetos "vivos" si el objeto es referenciado por cualquier otro cierre en el mismo contexto, sea o no si ese cierre en sí mismo es incluso accesible.

Mi pregunta es sobre la recolección de basura de cierre en otros navegadores (IE 9+ y Firefox). Estoy bastante familiarizado con las herramientas de webkit, como el generador de perfiles de montón de JavaScript, pero sé poco de las herramientas de otros navegadores, por lo que no he podido probar este.

En cuál de estos tres casos IE9 + y Firefox basura recoger el someClass ¿instancia?

Author: Paul Draper, 2013-11-06

6 answers

Por lo que puedo decir, esto no es un error, sino el comportamiento esperado.

Desde la página de administración de memoria de Mozilla : "A partir de 2012, todos los navegadores modernos envían un recolector de basura de marcado y barrido. Limitación"": los objetos deben ser explícitamente inalcanzables".

En sus ejemplos donde falla some todavía es accesible en el cierre. Probé dos maneras de hacerlo inalcanzable y ambas funcionan. O bien se establece some=null cuando ya no lo necesita, o usted establece window.f_ = null; y se habrá ido.

Update

Lo he probado en Chrome 30, FF25, Opera 12 y IE10 en Windows.

El estándar no dice nada sobre la recolección de basura, pero da algunas pistas de lo que debería suceder.

  • Sección 13 Definición de función, paso 4:"Sea closure el resultado de crear un nuevo objeto de función como se especifica en 13.2"
  • Sección 13.2 "Un Entorno léxico especificado por Scope" (scope = cierre)
  • Sección 10.2 Entornos léxicos:

" La referencia externa de un Entorno Léxico (interno) es una referencia al Entorno Léxico que lógicamente rodea el Ambiente Léxico interno.

Un entorno léxico externo puede, por supuesto, tener su propio entorno Entorno Léxico. Un Entorno Léxico puede servir como el entorno externo para múltiples Léxicos internos Ambiente. Por ejemplo, si una Declaración de función contiene dos Declaraciones de función anidadas entonces el Léxico Los entornos de cada una de las funciones anidadas tendrán como Entorno Léxico externo el Léxico Entorno de la ejecución actual de la función circundante."

Entonces, una función tendrá acceso al entorno del padre.

Por lo tanto, some debe estar disponible en el cierre de la función de retorno.

Entonces, ¿por qué no está siempre disponible?

Parece que Chrome y FF es lo suficientemente inteligente como para eliminar la variable en algunos casos, pero tanto en Opera como en IE la variable some está disponible en el cierre (NB: para ver esto establezca un punto de interrupción en return null y verifique el depurador).

El GC podría mejorarse para detectar si some se usa o no en las funciones, pero será complicado.

Un mal ejemplo:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

En el ejemplo anterior, el GC no tiene forma de saber si la variable se usa o no (código probado y funciona en Chrome30, FF25, Opera 12 e IE10).

La memoria se libera si la referencia al objeto se rompe asignando otro valor a window.f_.

En mi opinión esto no es un error.

 77
Author: some,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-13 04:38:09

Probé esto en IE9+ y Firefox.

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

Sitio en Vivo aquí.

Esperaba terminar con una matriz de 500 function() {}'s, usando una memoria mínima.

Desafortunadamente, ese no fue el caso. Cada función vacía mantiene un array (siempre inalcanzable, pero no GC'ed) de un millón de números.

Chrome finalmente se detiene y muere, Firefox termina todo después de usar casi 4 GB de RAM, e IE crece asintóticamente más lento hasta que se muestra " Fuera de memoria".

La eliminación de cualquiera de las líneas comentadas soluciona todo.

Parece que los tres navegadores (Chrome, Firefox e IE) mantienen un registro de entorno por contexto, no por cierre. Boris plantea la hipótesis de que la razón detrás de esta decisión es el rendimiento, y eso parece probable, aunque no estoy seguro de cuán eficaz se puede llamar a la luz del experimento anterior.

Si una necesidad de un cierre de referencia some (concedido no lo usé aquí, pero imagina que lo hice), si en lugar de

function g() { some; }

Utilizo

var g = (function(some) { return function() { some; }; )(some);

Solucionará los problemas de memoria moviendo el cierre a un contexto diferente a mi otra función.

Esto hará mi vida mucho más tediosa.

P.d. Por curiosidad, probé esto en Java (usando su capacidad para definir clases dentro de funciones). GC funciona como esperaba originalmente para Javascript.

 48
Author: Paul Draper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-08 03:00:46

La heurística varía, pero una forma común de implementar este tipo de cosas es crear un registro de entorno para cada llamada a f() en su caso, y solo almacenar los locales de f que están realmente cerrados (por algún cierre) en ese registro de entorno. Entonces cualquier cierre creado en la llamada a f mantiene vivo el registro de entorno. Creo que así es como Firefox implementa cierres, al menos.

Esto tiene los beneficios de un acceso rápido a variables cerradas y simplicidad de aplicación. Tiene el inconveniente del efecto observado, donde un cierre de corta duración el cierre sobre alguna variable hace que se mantenga vivo por cierres de larga duración.

Uno podría intentar crear múltiples registros de entorno para diferentes cierres, dependiendo de lo que realmente cierran, pero eso puede complicarse muy rápidamente y puede causar problemas de rendimiento y memoria propios...

 15
Author: Boris Zbarsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-06 04:09:27
  1. Mantener el estado entre llamadas a funciones Digamos que tienes la función add () y te gustaría que agregara todos los valores pasados en varias llamadas y devolviera la suma.

Como add (5); / / devuelve 5

Add (20); / / devuelve 25 (5+20)

Add (3); / / devuelve 28 (25 + 3)

De dos maneras se puede hacer esto primero es normal definir una variable global Por supuesto, puede usar una variable global para mantener el total. Pero ten en cuenta que este tipo te comerá vivo si (ab)usas globals.

Ahora la última forma usando closure con out define variable global

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());
 0
Author: Avinash Maurya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-12 03:30:38

function Country(){
    console.log("makesure country call");	
   return function State(){
   
    var totalstate = 0;	
	
	if(totalstate==0){	
	
	console.log("makesure statecall");	
	return function(val){
      totalstate += val;	 
      console.log("hello:"+totalstate);
	   return totalstate;
    }	
	}else{
	 console.log("hey:"+totalstate);
	}
	 
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d
 0
Author: Avinash Maurya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-12 04:17:25

(function(){

   function addFn(){

    var total = 0;
	
	if(total==0){	
	return function(val){
      total += val;	 
      console.log("hello:"+total);
	   return total+9;
    }	
	}else{
	 console.log("hey:"+total);
	}
	 
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
	console.log("r:"+r); //14 
	var r= add(20);  //25
	console.log("r:"+r); //34
	var r= add(10);  //35
	console.log("r:"+r);  //44
	
	
var addB = addFn();
	 var r= addB(6);  //6
	 var r= addB(4);  //10
	  var r= addB(19);  //29
    
  
}());
 0
Author: Avinash Maurya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-12 04:26:36