Cómo se recolectan los cierres de JavaScript

168

He registrado el siguiente error de Chrome , que ha provocado muchas pérdidas de memoria graves y no obvias en mi código:

(Estos resultados utilizan el generador de perfiles de memoria de Chrome Dev Tools , que ejecuta el GC, y luego toma una instantánea del montón de todo lo que no se ha recolectado).

En el siguiente código, la someClassinstancia es recolección de basura (buena):

var someClass = function() {};

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

window.f_ = f();

Pero no se recolectará basura en este caso (malo):

var someClass = function() {};

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

window.f_ = f();

Y la captura de pantalla correspondiente:

captura de pantalla de Chromebug

Parece que un cierre (en este caso function() {}) mantiene todos los objetos "vivos" si cualquier otro cierre hace referencia al objeto en el mismo contexto, ya sea que ese cierre sea 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 esto.

¿En cuál de estos tres casos IE9 + y Firefox recolectarán la someClass instancia?

Paul Draper
fuente
44
Para los no iniciados, ¿cómo le permite Chrome probar qué variables / objetos se recolectan y cuándo sucede?
nnnnnn
1
Tal vez la consola está haciendo referencia a ella. ¿Se pone GCed cuando borras la consola?
David
1
@david En el último ejemplo, la unreachablefunción nunca se ejecuta, por lo que no se registra realmente nada.
James Montagne
1
Tengo problemas para creer que pasó un error de esa importancia, incluso si parece que nos enfrentamos a los hechos. Sin embargo, estoy mirando el código una y otra vez y no encuentro ninguna otra explicación racional. ¿Intentó no ejecutar el código en la consola del todo bien (también conocido como dejar que el navegador lo ejecute naturalmente desde un script cargado)?
plalx
1
@ algunos, he leído ese artículo antes. Se subtitula "Manejo de referencias circulares en aplicaciones JavaScript", pero la preocupación de las referencias circulares JS / DOM no se aplica a ningún navegador moderno. Menciona cierres, pero en todos los ejemplos, las variables en cuestión todavía estaban en uso posible por el programa.
Paul Draper

Respuestas:

78

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

De la página de gestió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 hacerse explícitamente inalcanzables " .

En sus ejemplos donde falla sometodavía se puede acceder en el cierre. Intenté dos formas de hacerlo inalcanzable y ambas funcionan. O lo configura some=nullcuando ya no lo necesita, o lo configura window.f_ = null;y desaparecerá.

Actualizar

Lo he probado en Chrome 30, FF25, Opera 12 e 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: "Deje que el cierre sea 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" (alcance = 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 rodea lógicamente el entorno léxico interno.

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

Por lo tanto, una función tendrá acceso al entorno del padre.

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

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

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

El GC podría mejorarse para detectar si somese 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.

algunos
fuente
44
Pero, una vez que se setTimeout()ejecuta la devolución de llamada, se completa el alcance de la función de la setTimeout()devolución de llamada y se debe recopilar todo ese alcance, liberando su referencia some. Ya no hay ningún código que pueda ejecutarse que pueda alcanzar la instancia someen el cierre. Debe ser basura recolectada. El último ejemplo es aún peor porque unreachable()ni siquiera se llama y nadie tiene una referencia. Su alcance debe ser GCed también. Estos dos parecen errores. No hay ningún requisito de lenguaje en JS para "liberar" cosas en un ámbito de función.
jfriend00
1
@ Algunos no deberían. Se supone que las funciones no se cierran sobre las variables que no están utilizando internamente.
plalx
2
Se puede acceder mediante la función vacía, pero no es así, no hay referencias reales, por lo que debe quedar claro. La recolección de basura realiza un seguimiento de las referencias reales. No se supone que se aferre a todo lo que podría haber sido referenciado, solo a las cosas a las que realmente se hace referencia. Una vez que f()se llama al último , no hay referencias reales a somemás. Es inalcanzable y debe ser GCed.
jfriend00
1
@ jfriend00 No puedo encontrar nada en el (estándar) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] dice que solo las variables que usa internamente deberían estar disponibles. En la sección 13, el paso de producción 4: dejar que el cierre sea el resultado de crear un nuevo objeto Function como se especifica en 13.2 , 10.2 "La referencia del entorno externo se utiliza para modelar la anidación lógica de los valores del entorno léxico. La referencia externa de un (interno ) El entorno léxico es una referencia al entorno léxico que rodea lógicamente el entorno léxico interno ".
Algunos
2
Bueno, evales un caso realmente especial. Por ejemplo, evalno puede tener alias ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), por ejemplo var eval2 = eval. Si evalse usa (y dado que no se puede llamar con un nombre diferente, eso es fácil de hacer), entonces debemos suponer que puede usar cualquier cosa dentro del alcance.
Paul Draper el
49

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 serie de 500 function() {}'s, usando un mínimo de memoria.

Lamentablemente, ese no fue el caso. Cada función vacía se aferra a una matriz (de un alcance inalcanzable, pero no GC) 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 muestra "Sin memoria".

Eliminar cualquiera de las líneas comentadas arregla todo.

Parece que estos 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 qué tan eficaz puede llamarse a la luz del experimento anterior.

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

function g() { some; }

yo suelo

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

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

Esto hará que mi vida sea mucho más tediosa.

PD: Por curiosidad, intenté esto en Java (usando su capacidad para definir clases dentro de las funciones). GC funciona como esperaba originalmente Javascript.

Paul Draper
fuente
Creo que el paréntesis de cierre se perdió para la función externa var g = (function (some) {return function () {some;};}) (some);
HCJ
15

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

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

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

Boris Zbarsky
fuente
gracias por tu perspicacia He llegado a la conclusión de que también es así como Chrome implementa los cierres. Siempre pensé que se implementaron de la última manera, en que cada cierre guardaba solo el entorno que necesitaba, pero ese no es el caso. Me pregunto si es realmente tan complicado crear múltiples registros de entorno. En lugar de agregar las referencias de los cierres, actúe como si cada uno fuera el único cierre. Supuse que las consideraciones de rendimiento eran el razonamiento aquí, aunque para mí las consecuencias de tener un registro de entorno compartido me parecen aún peores.
Paul Draper
La última forma en algunos casos conduce a una explosión en el número de registros ambientales que deben crearse. A menos que se esfuerce por compartirlas entre las funciones cuando pueda, pero luego necesita un montón de maquinaria complicada para hacerlo. Es posible, pero me dijeron que las compensaciones de rendimiento favorecen el enfoque actual.
Boris Zbarsky
El número de registros es igual al número de cierres creados. Podría describirlo O(n^2)o O(2^n)como una explosión, pero no como un aumento proporcional.
Paul Draper
Bueno, O (N) es una explosión en comparación con O (1), especialmente cuando cada uno puede ocupar una buena cantidad de memoria ... Nuevamente, no soy un experto en esto; Es probable que preguntar en el canal #jsapi de irc.mozilla.org le brinde una explicación mejor y más detallada de lo que puedo proporcionar sobre las compensaciones.
Boris Zbarsky el
1
@Esailija En realidad es bastante común, desafortunadamente. Todo lo que necesita es un gran temporal en la función (generalmente una gran matriz tipada) que utilizan algunas devoluciones de llamada aleatorias de corta duración y un cierre de larga duración. Ha aparecido varias veces recientemente para las personas que escriben aplicaciones web ...
Boris Zbarsky
0
  1. Mantener el estado entre llamadas a funciones Supongamos que tiene la función add () y desea que agregue todos los valores que se le pasaron en varias llamadas y devuelva la suma.

como add (5); // devuelve 5

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

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

dos formas en que 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 de usar el cierre sin definir la 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));
  
}());

Avinash Maurya
fuente
0

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

Avinash Maurya
fuente
por favor describa la respuesta
janith1024
0

(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
    
  
}());

Avinash Maurya
fuente