Wie JavaScript-Schließungen durch Müll gesammelt werden

168

Ich habe den folgenden Chrome-Fehler protokolliert , der zu vielen schwerwiegenden und nicht offensichtlichen Speicherverlusten in meinem Code geführt hat:

(Diese Ergebnisse verwenden den Speicherprofiler von Chrome Dev Tools , mit dem der GC ausgeführt wird, und erstellen dann einen Heap-Snapshot von allem, was nicht gesammelt wurde.)

Im folgenden Code ist die someClassInstanz Müll gesammelt (gut):

var someClass = function() {};

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

window.f_ = f();

In diesem Fall wird jedoch kein Müll gesammelt (schlecht):

var someClass = function() {};

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

window.f_ = f();

Und der entsprechende Screenshot:

Screenshot von Chromebug

Es scheint, dass ein Abschluss (in diesem Fall function() {}) alle Objekte "am Leben" hält, wenn das Objekt von einem anderen Abschluss im selben Kontext referenziert wird, unabhängig davon, ob dieser Abschluss selbst erreichbar ist oder nicht.

Meine Frage betrifft die Speicherbereinigung der Schließung in anderen Browsern (IE 9+ und Firefox). Ich bin mit den Tools des Webkits wie dem JavaScript-Heap-Profiler ziemlich vertraut, kenne aber nur wenige Tools anderer Browser, sodass ich dies nicht testen konnte.

In welchem ​​dieser drei Fälle sammeln IE9 + und Firefox-Garbage die someClass Instanz?

Paul Draper
quelle
4
Wie können Sie in Chrome für Uneingeweihte testen, welche Variablen / Objekte im Müll gesammelt werden und wann dies geschieht?
nnnnnn
1
Vielleicht behält die Konsole einen Verweis darauf. Wird es GCed, wenn Sie die Konsole löschen?
david
1
@david Im letzten Beispiel wird die unreachableFunktion nie ausgeführt, sodass tatsächlich nichts protokolliert wird.
James Montagne
1
Es fällt mir schwer zu glauben, dass ein Fehler von dieser Bedeutung aufgetreten ist, auch wenn wir mit den Fakten konfrontiert zu sein scheinen. Ich schaue jedoch immer wieder auf den Code und finde keine andere rationelle Erklärung. Sie haben versucht, den Code überhaupt nicht in der Konsole auszuführen (auch bekannt als, dass der Browser ihn auf natürliche Weise über ein geladenes Skript ausführt)?
Plalx
1
@ einige, ich habe diesen Artikel schon einmal gelesen. Es trägt den Untertitel "Umgang mit Zirkelverweisen in JavaScript-Anwendungen", aber das Anliegen von JS / DOM-Zirkelverweisen gilt für keinen modernen Browser. Es werden Schließungen erwähnt, aber in allen Beispielen wurden die fraglichen Variablen vom Programm noch verwendet.
Paul Draper

Antworten:

78

Soweit ich das beurteilen kann, handelt es sich nicht um einen Fehler, sondern um das erwartete Verhalten.

Auf der Speicherverwaltungsseite von Mozilla : "Ab 2012 liefern alle modernen Browser einen Mark-and-Sweep-Garbage-Collector aus." "Einschränkung: Objekte müssen explizit unerreichbar gemacht werden " .

In Ihren Beispielen, wo es fehlschlägt, someist im Verschluss noch erreichbar. Ich habe zwei Möglichkeiten ausprobiert, um es unerreichbar zu machen, und beide funktionieren. Entweder Sie setzen, some=nullwenn Sie es nicht mehr brauchen, oder Sie setzen window.f_ = null;und es wird weg sein.

Aktualisieren

Ich habe es in Chrome 30, FF25, Opera 12 und IE10 unter Windows versucht.

Der Standard sagt nichts über die Speicherbereinigung aus, gibt aber einige Hinweise darauf, was passieren sollte.

  • Abschnitt 13 Funktionsdefinition, Schritt 4: "Der Abschluss sei das Ergebnis der Erstellung eines neuen Funktionsobjekts gemäß 13.2."
  • Abschnitt 13.2 "Eine durch Scope angegebene lexikalische Umgebung" (scope = Schließung)
  • Abschnitt 10.2 Lexikalische Umgebungen:

"Die äußere Referenz einer (inneren) lexikalischen Umgebung ist eine Referenz auf die lexikalische Umgebung, die die innere lexikalische Umgebung logisch umgibt.

Eine äußere lexikalische Umgebung kann natürlich eine eigene äußere lexikalische Umgebung haben. Eine lexikalische Umgebung kann als äußere Umgebung für mehrere innere lexikalische Umgebungen dienen. Wenn eine Funktionsdeklaration beispielsweise zwei verschachtelte Funktionsdeklarationen enthält, haben die lexikalischen Umgebungen jeder der verschachtelten Funktionen als äußere lexikalische Umgebung die lexikalische Umgebung der aktuellen Ausführung der umgebenden Funktion. "

Eine Funktion hat also Zugriff auf die Umgebung des übergeordneten Elements.

Sollte somealso beim Schließen der Rückgabefunktion verfügbar sein.

Warum ist es dann nicht immer verfügbar?

Es scheint, dass Chrome und FF klug genug sind, um die Variable in einigen Fällen zu entfernen, aber sowohl in Opera als auch in IE ist die someVariable im Closure verfügbar (Hinweis: Um dies anzuzeigen, setzen Sie einen Haltepunkt return nullund überprüfen Sie den Debugger).

Der GC könnte verbessert werden, um festzustellen, ob er somein den Funktionen verwendet wird oder nicht, aber er wird kompliziert.

Ein schlechtes Beispiel:

var someClass = function() {};

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

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

Im obigen Beispiel kann der GC nicht feststellen, ob die Variable verwendet wird oder nicht (Code getestet und funktioniert in Chrome30, FF25, Opera 12 und IE10).

Der Speicher wird freigegeben, wenn der Verweis auf das Objekt durch Zuweisen eines anderen Werts unterbrochen wird window.f_.

Meiner Meinung nach ist dies kein Fehler.

etwas
quelle
4
Sobald der setTimeout()Rückruf ausgeführt wird, ist dieser Funktionsumfang des setTimeout()Rückrufs abgeschlossen, und der gesamte Bereich sollte mit Müll gesammelt werden, wobei der Verweis auf freigegeben wird some. Es kann kein Code mehr ausgeführt werden, der die Instanz someim Abschluss erreichen kann. Es sollte Müll gesammelt werden. Das letzte Beispiel ist noch schlimmer, weil unreachable()es nicht einmal aufgerufen wird und niemand einen Hinweis darauf hat. Sein Geltungsbereich sollte ebenfalls überprüft werden. Diese beiden scheinen wie Fehler. In JS gibt es keine Sprachanforderung, um Dinge in einem Funktionsumfang "freizugeben".
jfriend00
1
@some Es sollte nicht. Funktionen sollen nicht über Variablen geschlossen werden, die sie nicht intern verwenden.
Plalx
2
Es könnte über die leere Funktion darauf zugegriffen werden, aber es gibt keine tatsächlichen Verweise darauf, daher sollte es klar sein. Die Speicherbereinigung verfolgt die tatsächlichen Referenzen. Es soll nicht an allem festhalten, worauf verwiesen werden könnte, sondern nur an den Dingen, auf die tatsächlich verwiesen wird. Sobald der letzte f()aufgerufen wurde, gibt es keine tatsächlichen Verweise somemehr. Es ist nicht erreichbar und sollte GCed sein.
jfriend00
1
@ jfriend00 Ich kann nichts im (Standard) finden [ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] sagt nichts über nur die Variablen aus, die intern verwendet werden sollten. In Abschnitt 13, Produktionsschritt 4: Schließen soll das Ergebnis der Erstellung eines neuen Funktionsobjekts sein, wie in 13.2 , 10.2 angegeben. "Die Referenz für die äußere Umgebung wird verwendet, um die logische Verschachtelung von Werten für die lexikalische Umgebung zu modellieren. Die äußere Referenz für a (inner ) Lexikalische Umgebung ist ein Verweis auf die lexikalische Umgebung, die die innere lexikalische Umgebung logisch umgibt. "
einige
2
Nun, evalist wirklich ein Sonderfall. Zum Beispiel evalkann nicht aliased (werden developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... ), zB var eval2 = eval. Wenn evales verwendet wird (und da es nicht unter einem anderen Namen aufgerufen werden kann, ist dies einfach), müssen wir davon ausgehen, dass es alles im Umfang verwenden kann.
Paul Draper
49

Ich habe dies in IE9 + und Firefox getestet.

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

Live Seite hier .

Ich hoffte, mit einem Array von 500 function() {}mit minimalem Speicherplatz fertig zu werden.

Das war leider nicht der Fall. Jede leere Funktion enthält ein (für immer nicht erreichbares, aber nicht GC-fähiges) Array mit einer Million Zahlen.

Chrome wird schließlich angehalten und stirbt, Firefox beendet das Ganze, nachdem fast 4 GB RAM verwendet wurden, und der IE wächst asymptotisch langsamer, bis "Nicht genügend Speicher" angezeigt wird.

Durch Entfernen einer der kommentierten Zeilen wird alles behoben.

Es scheint, dass alle drei dieser Browser (Chrome, Firefox und IE) eine Umgebungsaufzeichnung pro Kontext und nicht pro Abschluss führen. Boris vermutet, dass der Grund für diese Entscheidung die Leistung ist, und das scheint wahrscheinlich, obwohl ich nicht sicher bin, wie performant sie angesichts des obigen Experiments genannt werden kann.

Wenn ein Verweis benötigt wird some(vorausgesetzt, ich habe ihn hier nicht verwendet, aber stellen Sie sich vor, ich hätte ihn verwendet), wenn statt

function g() { some; }

ich benutze

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

Es wird die Speicherprobleme beheben, indem der Abschluss in einen anderen Kontext als meine andere Funktion verschoben wird.

Dies wird mein Leben viel langweiliger machen.

PS Aus Neugier habe ich dies in Java versucht (mit seiner Fähigkeit, Klassen innerhalb von Funktionen zu definieren). GC funktioniert so, wie ich es mir ursprünglich erhofft hatte.

Paul Draper
quelle
Ich denke, das Schließen der Klammer für die äußere Funktion fehlte var g = (function (some) {return function () {some;};}) (some);
HCJ
15

Die Heuristiken variieren, aber eine übliche Methode zur Implementierung dieser Art besteht darin, für jeden Anruf f()in Ihrem Fall einen Umgebungsdatensatz zu erstellen und nur die Einheimischen zu speichern f, die tatsächlich (durch einen Abschluss) in diesem Umgebungsdatensatz geschlossen sind. Dann jede Schließung, die im Aufruf erstellt wurde, um fden Umgebungsdatensatz am Leben zu erhalten. Ich glaube, so implementiert Firefox zumindest Verschlüsse.

Dies hat den Vorteil eines schnellen Zugriffs auf geschlossene Variablen und einer einfachen Implementierung. Es hat den Nachteil des beobachteten Effekts, dass ein kurzlebiger Verschluss, der über eine Variable schließt, dazu führt, dass er durch langlebige Verschlüsse am Leben erhalten wird.

Man könnte versuchen, mehrere Umgebungsdatensätze für verschiedene Schließungen zu erstellen, je nachdem, was sie tatsächlich schließen, aber das kann sehr schnell sehr kompliziert werden und selbst Leistungs- und Speicherprobleme verursachen ...

Boris Zbarsky
quelle
Vielen Dank für Ihren Einblick. Ich bin zu dem Schluss gekommen, dass Chrome auch so Schließungen implementiert. Ich habe immer gedacht, dass sie auf die letztere Art und Weise implementiert werden, bei der jeder Abschluss nur die Umgebung enthält, die er benötigt, aber dies ist nicht der Fall. Ich frage mich, ob es wirklich so kompliziert ist, mehrere Umgebungsdatensätze zu erstellen. Anstatt die Referenzen der Schließungen zu aggregieren, tun Sie so, als wäre jede die einzige Schließung. Ich hatte vermutet, dass hier Leistungsüberlegungen die Begründung waren, obwohl mir die Konsequenzen einer gemeinsamen Umgebungsaufzeichnung noch schlimmer erscheinen.
Paul Draper
Der letztere Weg führt in einigen Fällen zu einer Explosion der Anzahl der Umgebungsdatensätze, die erstellt werden müssen. Es sei denn, Sie bemühen sich, sie funktionsübergreifend zu teilen, wenn Sie können, aber dann benötigen Sie eine Reihe komplizierter Maschinen, um dies zu tun. Es ist möglich, aber mir wurde gesagt, dass die Leistungskompromisse den aktuellen Ansatz begünstigen.
Boris Zbarsky
Die Anzahl der Datensätze entspricht der Anzahl der erstellten Abschlüsse. Ich könnte beschrieben O(n^2)oder O(2^n)als eine Explosion, aber nicht eine proportionale Zunahme.
Paul Draper
Nun, O (N) ist eine Explosion im Vergleich zu O (1), besonders wenn jeder eine angemessene Menge an Speicherplatz beanspruchen kann ... Auch hier bin ich kein Experte; Wenn Sie auf dem # jsapi-Kanal auf irc.mozilla.org nachfragen, erhalten Sie wahrscheinlich eine bessere und detailliertere Erklärung, als ich Ihnen die Kompromisse liefern kann.
Boris Zbarsky
1
@Esailija Es ist eigentlich leider ziemlich häufig. Alles, was Sie benötigen, ist eine große temporäre Funktion (normalerweise ein großes typisiertes Array), die von einem zufälligen kurzlebigen Rückruf verwendet wird, und ein langlebiger Abschluss. Es ist in letzter Zeit einige Male für Leute
aufgetaucht
0
  1. Status zwischen Funktionsaufrufen beibehalten Angenommen, Sie haben die Funktion add () und möchten, dass alle in mehreren Aufrufen übergebenen Werte hinzugefügt werden und die Summe zurückgegeben wird.

wie add (5); // gibt 5 zurück

addiere (20); // gibt 25 (5 + 20) zurück

addiere (3); // gibt 28 (25 + 3) zurück

Zwei Möglichkeiten, wie Sie dies zuerst tun können, sind normal. Definieren Sie eine globale Variable. Natürlich können Sie eine globale Variable verwenden, um die Summe zu speichern. Aber denken Sie daran, dass dieser Typ Sie lebendig essen wird, wenn Sie (ab) Globals verwenden.

Jetzt neueste Methode mit Closure ohne definierte globale Variable

(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
quelle
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
quelle
Bitte beschreiben Sie die Antwort
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
quelle