Comment les fermetures JavaScript sont récupérées

168

J'ai enregistré le bogue Chrome suivant , qui a conduit à de nombreuses fuites de mémoire graves et non évidentes dans mon code:

(Ces résultats utilisent le profileur de mémoire de Chrome Dev Tools , qui exécute le GC, puis prend un instantané du tas de tout ce qui n'a pas été collecté.)

Dans le code ci-dessous, l' someClassinstance est garbage collecté (bon):

var someClass = function() {};

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

window.f_ = f();

Mais ce ne sera pas ramassé dans ce cas (mauvais):

var someClass = function() {};

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

window.f_ = f();

Et la capture d'écran correspondante:

capture d'écran de Chromebug

Il semble qu'une fermeture (dans ce cas, function() {}) garde tous les objets «vivants» si l'objet est référencé par une autre fermeture dans le même contexte, que cette fermeture soit même accessible ou non.

Ma question porte sur le ramassage des ordures de fermeture dans d'autres navigateurs (IE 9+ et Firefox). Je connais assez bien les outils de webkit, tels que le profileur de tas JavaScript, mais je connais peu les outils des autres navigateurs, donc je n'ai pas pu tester cela.

Dans lequel de ces trois cas IE9 + et Firefox collecteront-ils l' someClass instance?

Paul Draper
la source
4
Pour les non-initiés, comment Chrome vous permet-il de tester les variables / objets qui sont récupérés et quand cela se produit?
nnnnnn
1
Peut-être que la console en garde une référence. Est-ce qu'il est GC lorsque vous effacez la console?
david
1
@david Dans le dernier exemple, la unreachablefonction n'est jamais exécutée, donc rien n'est réellement enregistré.
James Montagne
1
J'ai du mal à croire qu'un bug de cette importance est passé, même si nous semblons être confrontés aux faits. Cependant, je regarde le code encore et encore et je ne trouve aucune autre explication rationnelle. Vous avez essayé de ne pas exécuter du tout le code dans la console (alias laisser le navigateur l'exécuter naturellement à partir d'un script chargé)?
plalx
1
@some, j'ai déjà lu cet article. Il est sous-titré "Gestion des références circulaires dans les applications JavaScript", mais le souci des références circulaires JS / DOM ne s'applique à aucun navigateur moderne. Il mentionne des fermetures, mais dans tous les exemples, les variables en question étaient encore utilisables par le programme.
Paul Draper

Réponses:

78

Autant que je sache, ce n'est pas un bug mais le comportement attendu.

À partir de la page de gestion de la mémoire de Mozilla : «À partir de 2012, tous les navigateurs modernes embarquent un ramasse-miettes avec marquage et balayage.» "Limitation: les objets doivent être rendus explicitement inaccessibles " .

Dans vos exemples où il échoue, il someest toujours accessible dans la fermeture. J'ai essayé deux façons de le rendre inaccessible et les deux fonctionnent. Soit vous définissez some=nulllorsque vous n'en avez plus besoin, soit vous définissez window.f_ = null;et il disparaîtra.

Mettre à jour

Je l'ai essayé dans Chrome 30, FF25, Opera 12 et IE10 sous Windows.

La norme ne dit rien sur le ramasse-miettes, mais donne quelques indices sur ce qui devrait se passer.

  • Section 13 Définition de fonction, étape 4: "Soit la fermeture le résultat de la création d'un nouvel objet Function comme spécifié en 13.2"
  • Section 13.2 «Un environnement lexical spécifié par Scope» (scope = clôture)
  • Section 10.2 Environnements lexicaux:

«La référence externe d'un environnement lexical (interne) est une référence à l'environnement lexical qui entoure logiquement l'environnement lexical interne.

Un environnement lexical externe peut, bien entendu, avoir son propre environnement lexical externe. Un environnement lexical peut servir d'environnement externe pour plusieurs environnements lexicaux internes. Par exemple, si une déclaration de fonction contient deux déclarations de fonction imbriquées , les environnements lexicaux de chacune des fonctions imbriquées auront comme environnement lexical externe l'environnement lexical de l'exécution actuelle de la fonction environnante. "

Ainsi, une fonction aura accès à l'environnement du parent.

Donc, somedevrait être disponible dans la fermeture de la fonction de retour.

Alors pourquoi n'est-il pas toujours disponible?

Il semble que Chrome et FF soient suffisamment intelligents pour éliminer la variable dans certains cas, mais dans Opera et IE, la somevariable est disponible dans la fermeture (NB: pour afficher cela, définissez un point d'arrêt return nullet vérifiez le débogueur).

Le GC pourrait être amélioré pour détecter s'il someest utilisé ou non dans les fonctions, mais ce sera compliqué.

Un mauvais exemple:

var someClass = function() {};

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

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

Dans l'exemple ci-dessus le GC n'a aucun moyen de savoir si la variable est utilisée ou non (code testé et fonctionne sous Chrome30, FF25, Opera 12 et IE10).

La mémoire est libérée si la référence à l'objet est rompue en attribuant une autre valeur à window.f_.

À mon avis, ce n'est pas un bug.

certains
la source
4
Mais, une fois le setTimeout()rappel exécuté, la portée de la fonction du setTimeout()rappel est terminée et toute cette portée doit être garbage collection, libérant sa référence à some. Il n'y a plus de code exécutable pouvant atteindre l'instance de somedans la fermeture. Il devrait être ramassé des ordures. Le dernier exemple est encore pire car il unreachable()n'est même pas appelé et personne n'y fait référence. Sa portée devrait également être GC. Ces deux semblent être des bugs. Il n'y a aucune exigence de langage dans JS pour «libérer» des éléments dans une portée de fonction.
jfriend00
1
@some Ça ne devrait pas. Les fonctions ne sont pas censées se fermer sur les variables qu'elles n'utilisent pas en interne.
plalx
2
Il pourrait être accédé par la fonction vide, mais ce n'est pas le cas, donc il n'y a pas de références réelles, donc cela devrait être clair. Le garbage collection assure le suivi des références réelles. Il n'est pas censé conserver tout ce qui aurait pu être référencé, seulement les choses qui sont réellement référencées. Une fois le dernier f()appelé, il n'y a plus de références réelles some. Il est inaccessible et doit être GC.
jfriend00
1
@ jfriend00 Je ne trouve rien dans le (standard) [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] qui dit que seules les variables qu'il utilise en interne devraient être disponibles. Dans la section 13, étape de production 4: Soit la fermeture le résultat de la création d'un nouvel objet Function comme spécifié en 13.2 , 10.2 "La référence d'environnement externe est utilisée pour modéliser l'imbrication logique des valeurs d'environnement lexical. La référence externe d'un (interne ) L'environnement lexical est une référence à l'environnement lexical qui entoure logiquement l'environnement lexical interne. "
certains
2
Eh bien, evalc'est un cas vraiment spécial. Par exemple, evalne peut pas être aliasé ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), par exemple var eval2 = eval. Si evalest utilisé (et comme il ne peut pas être appelé par un nom différent, c'est facile à faire), alors nous devons supposer qu'il peut utiliser n'importe quoi dans la portée.
Paul Draper
49

J'ai testé cela dans IE9 + et 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);

Site en direct ici .

J'espérais me retrouver avec un tableau de 500 function() {}, en utilisant une mémoire minimale.

Ce n’était malheureusement pas le cas. Chaque fonction vide tient à un tableau (à jamais inaccessible, mais pas GC) d'un million de nombres.

Chrome s'arrête finalement et meurt, Firefox termine le tout après avoir utilisé près de 4 Go de RAM, et IE devient asymptotiquement plus lent jusqu'à ce qu'il affiche "Out of memory".

La suppression de l'une des lignes commentées corrige tout.

Il semble que ces trois navigateurs (Chrome, Firefox et IE) conservent un enregistrement d'environnement par contexte et non par fermeture. Boris émet l'hypothèse que la raison de cette décision est la performance, et cela semble probable, même si je ne suis pas sûr de la performance que cela peut avoir à la lumière de l'expérience ci-dessus.

Si vous avez besoin d'un référencement de fermeture some(d'accord, je ne l'ai pas utilisé ici, mais imaginez que je l'ai fait), si au lieu de

function g() { some; }

j'utilise

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

cela résoudra les problèmes de mémoire en déplaçant la fermeture dans un contexte différent de mon autre fonction.

Cela rendra ma vie beaucoup plus fastidieuse.

PS Par curiosité, j'ai essayé cela en Java (en utilisant sa capacité à définir des classes à l'intérieur des fonctions). GC fonctionne comme je l'avais initialement espéré pour Javascript.

Paul Draper
la source
Je pense que la fermeture des parenthèses a manqué pour la fonction externe var g = (function (some) {return function () {some;};}) (some);
HCJ
15

Les heuristiques varient, mais une manière courante d'implémenter ce genre de chose consiste à créer un enregistrement d'environnement pour chaque appel f()dans votre cas, et à ne stocker que les sections locales fqui sont effectivement fermées (par une fermeture) dans cet enregistrement d'environnement. Ensuite, toute fermeture créée dans l'appel à fmaintient en vie l'enregistrement d'environnement. Je pense que c'est ainsi que Firefox implémente au moins les fermetures.

Cela présente les avantages d'un accès rapide aux variables fermées et d'une simplicité de mise en œuvre. Elle présente l'inconvénient de l'effet observé, où une fermeture de courte durée sur une variable la maintient en vie par des fermetures de longue durée.

On pourrait essayer de créer plusieurs enregistrements d'environnement pour différentes fermetures, en fonction de ce qu'ils ferment réellement, mais cela peut devenir très compliqué très rapidement et peut causer des problèmes de performances et de mémoire.

Boris Zbarsky
la source
merci pour votre perspicacité. J'en suis venu à conclure que c'est également ainsi que Chrome implémente les fermetures. J'ai toujours pensé qu'ils étaient mis en œuvre de cette dernière manière, dans laquelle chaque fermeture ne gardait que l'environnement dont elle avait besoin, mais ce n'est pas le cas. Je me demande s'il est vraiment si compliqué de créer plusieurs enregistrements d'environnement. Plutôt que d'agréger les références des fermetures, agissez comme si chacune était la seule fermeture. J'avais deviné que les considérations de performance étaient le raisonnement ici, bien que pour moi les conséquences d'avoir un enregistrement d'environnement partagé semblent encore pires.
Paul Draper
Cette dernière manière conduit dans certains cas à une explosion du nombre d'enregistrements d'environnement qui doivent être créés. À moins que vous n'essayiez de les partager entre les fonctions lorsque vous le pouvez, mais vous avez alors besoin d'un tas de machines compliquées pour le faire. C'est possible, mais on me dit que les compromis de performance favorisent l'approche actuelle.
Boris Zbarsky
Le nombre d'enregistrements est égal au nombre de fermetures créées. Je pourrais décrire O(n^2)ou O(2^n)comme une explosion, mais pas une augmentation proportionnelle.
Paul Draper
Eh bien, O (N) est une explosion par rapport à O (1), surtout quand chacun peut occuper une bonne quantité de mémoire ... Encore une fois, je ne suis pas un expert en la matière; demander sur le canal #jsapi sur irc.mozilla.org est susceptible de vous fournir une explication meilleure et plus détaillée que je ne peux fournir de ce que sont les compromis.
Boris Zbarsky
1
@Esailija C'est en fait assez courant, malheureusement. Tout ce dont vous avez besoin est un grand temporaire dans la fonction (généralement un grand tableau typé) que certains rappels aléatoires de courte durée utilisent et une fermeture de longue durée. Il est apparu plusieurs fois récemment pour les personnes qui écrivent des applications Web ...
Boris Zbarsky
0
  1. Maintenir l'état entre les appels de fonction Supposons que vous ayez la fonction add () et que vous souhaitiez qu'elle ajoute toutes les valeurs qui lui sont passées en plusieurs appels et renvoie la somme.

comme add (5); // renvoie 5

ajouter (20); // renvoie 25 (5 + 20)

ajouter (3); // renvoie 28 (25 + 3)

la première façon de procéder est de définir une variable globale. Bien sûr, vous pouvez utiliser une variable globale pour contenir le total. Mais gardez à l'esprit que ce mec vous mangera vivant si vous (ab) utilisez des globals.

maintenant la dernière façon d' utiliser la fermeture sans définir la variable globale

(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
la source
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
la source
veuillez décrire la réponse
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
la source