molily: Closures und Garbage Collection

Hallo,

ich baue gerade eine Web-App für den Mobile Safari, bei der alle Daten mit Ajax nachgeladen werden. D.h. ein und dasselbe Dokument bleibt sehr lange geöffnet. Ich habe zwar noch keine Anzeichen von vollgelaufenen Speicher, aber ich mache mir Gedanken darum, ob der Garbage Collector die ständig angelegten und über siebenunzwanzig Callbacks herumgereichten Objekte irgendwann korrekt löscht.

Dazu habe ich Closures (verschachtelte Funktionen) soweit es geht vermieden, sodass Variablen möglichst nicht eingeschlossen und konserviert werden. Dummerweise weiß ich nicht, wie ich so etwas kontrollieren kann. Ich dachte, man könne Variablen mit delete aus der Welt schaffen, aber das geht gerade bei mit »var« deklarierten lokalen Variablen nicht (ist so in ECMAScript definiert - ich rall es einfach nicht):

function bla () {
  var lokal = {}; // Soll nicht eingeschlossen werden
  var enclosed = {}; // Soll eingeschlossen werden
  weitereFunc(lokal, enclosed);
  $('#bla').click(function closure () {
     alert(enclosed);
  });
  // Versuche ich zu vermeiden z.B. mit Kontrolliertem Anlegen von Closures via Currying
  // Legt aber auch jedes Mal Funktionen an. Wenn die beim Löschen der Elemente bzw. der Handler nicht weggeräumt werden, dann läuft der Speicher irgendwann voll
  $('#bla').click( externerHandler.curry(enclosed) );
  delete lokal; // geht nicht per definitionem
  lokal = undefined; // Objekt händisch löschen - löscht aber nur diese eine Referenz darauf
}

Problem ist nun, dass ich viele »weitereFunc« habe, die allesamt das Object by Reference übergeben bekommen. Theoretisch müsste »lokal« nach Ablauf dieser Funktionen gelöscht werden, wenn diese keine Closure beinhalten. Allerdings ist das JS mittlerweile so groß, dass ich das gerne automatisiert testen würde. Ich weiß aber nicht, wie ich Einblick in den Stack des JS-Interpreters gewinnen kann. Ich vermute, dass lokal = undefined nur die lokale Variable überschreibt, aber wenn das Object noch woanders eingeschlossen ist, existieren weitere Referenzen darauf und der GC übersieht es.

Ein schwerwiegenderes Problem scheinen mir Referenzen auf DOM-Objekte, die in Closures eingeschlossen werden. Das DOM wird ja ständig geändert, sodass der GC alte, ausgehängte DOM-Objekte löschen müsste. Tut er das aber z.B. wegen Closures nicht, dürfte der Speicher meiner Ansicht nach schnell volllaufen. Das kann ich wie gesagt noch nicht beobachten (auf dem Desktop-Safari), aber ich würde es gerne prüfen können.

Habt ihr dazu Ideen?

Mathias

  1. Mit var innerhalb einer Funktion definierte Werte bleiben nur bis zum Abschluß der Funktion im Speicher und werden danach vom Garbage Collector eingesammelt. Ein myvar = undefined überschreibt nicht den Speicherbereich, sondern nur den Inhalt, bis die Variable ganz freigegeben wird.

    Hinzu kommt, dass der Mobile Safari das Speichermanagement von Cocoa verwendet, welches nur über rudimentäre Garbage Collection verfügt.

    Gruß, LX

    --
    RFC 1925, Satz 3: Mit ausreichendem Schub fliegen Schweine wunderbar. (...)
    1. Mit var innerhalb einer Funktion definierte Werte bleiben nur bis zum Abschluß der Funktion im Speicher und werden danach vom Garbage Collector eingesammelt. Ein myvar = undefined überschreibt nicht den Speicherbereich, sondern nur den Inhalt, bis die Variable ganz freigegeben wird.

      Naja, aber bei Closures kehrt sich die Geschichte um. In dessen Scope wird eine Referenz auf die Variable angelegt, und wenn die innere Funktion die äußere überlebt, dann kann wird sie nicht vom GC gelöscht, solange die innere Funktion existiert. In dem Fall hilft m.W. das überschreiben - wenn ich schon nicht das Eingeschlossensein ändern kann, dann kann ich damit zumindest die Datenmenge reduzieren.

      Mathias

      1. Das Überschreiben hilft an dieser Stelle nur innerhalb des globalen Scopes - Du müßtest die Funktion und die Closure überschreiben.

        Gruß, LX

        --
        RFC 1925, Satz 3: Mit ausreichendem Schub fliegen Schweine wunderbar. (...)
    2. Hallo LX,

      Hinzu kommt, dass der Mobile Safari das Speichermanagement von Cocoa verwendet, welches nur über rudimentäre Garbage Collection verfügt.

      Huh? Soweit ich weiß, besteht Webkit da von KHTML abstammend praktisch nur aus C++; das gilt auch für JavascriptCore bzw. die Neuentwicklungen. Wie kommst Du auf Cocoa? Wird bei Mobile Safari nicht der GC von JSCore verwendet? Und was genau ist an Cocoas GC rudimentär?

      Tim

  2. Hallo,

    Habt ihr dazu Ideen?

    Das naheliegende, was mir einfällt ist: Ausprobieren. Mit einem Tool, das die Speicherbelegung anzeigt und einer kleinen "Killer-Application" in JS, die exzessiv das Horror-Szenario zu produzieren versucht. Wenn die Speicherbelegung laufend ansteigt, hast du das Problem am Wickel, und kannst dann die Killer-App solange modifizieren, bis es verschwindet.

    BTW, was ist denn $('#bla') für ein Ausdruck? Ich ahne es zwar, aber sowas hab'ich noch nie benutzt. Kanst du es kurz erklären?

    Gruß, Don P

    1. Das naheliegende, was mir einfällt ist: Ausprobieren. Mit einem Tool, das die Speicherbelegung anzeigt und einer kleinen "Killer-Application" in JS, die exzessiv das Horror-Szenario zu produzieren versucht.

      Das habe ich schon ein wenig versucht mit dem Desktop-Safari, allerdings nicht so intensiv. Das werde ich einmal nachholen. Bei den recht kleinen Tests ist mir soweit nichts aufgefallen. Die Daten, mit denen ich arbeite, sind für sich genommen nicht so groß, insofern fällt das beim Desktop-Safari vermutlich aus Prinzip gar nicht auf. Im Leerlauf ändert der Safari-Prozess auch mal eben seine Speicherbelegung im Megabyte-Bereich - das ist beim Desktop egal, beim iPhone kann das einen Crash bedeuten. Deshalb würde ich es gerne etwas grundlegender angehen.

      BTW, was ist denn $('#bla') für ein Ausdruck?

      Eine Helferfunktion von jQuery, Prototype, Mootools usw. zum DOM-Querying über CSS-Selektoren.

      Mathias

  3. Moin.

    Falls die in Safari eingesetzte JS Engine hinreichend gut optimiert, tritt das beschriebene Problem erst gar nicht auf: Da in den inneren Funktionen kein eval() vorkommt, muss die Closure keine Referenz auf die komplette lexikale Umgebung der äußeren Funktion bereithalten, sondern nur Referenzen auf die tatsächlich benötigten Objekte.

    Falls die Engine tatsächlich die komplette lexikale Umgebung referenziert, sollte das Überschreiben von lokal mit undefined oder null das Problem wie gewünscht lösen. Ein delete würde zusätzlich zum Löschen der Objektreferenz auch noch den für die Referenz selbst benötigten Speicher (den Pointer in der Slot-Tabelle des Objekts) freigeben, was aber für das Variablen-Objekt einer Funktion (auf das ohnehin nicht direkt zugegriffen werden kann) nicht vorgesehen ist (ES5 in strict-mode wirft hier sogar einen ReferenceError).

    Im IE ist besondere Vorsicht geboten, da dort DOM-Knoten über reference-counting entsorgt werden, d.h. zirkuläre Referenzen führen zu Speicherlecks (beachte Listing 5: Event handling memory leak pattern).

    Ansonsten können Closures als Event-Handler oft ganz vermieden werden, indem man den DOM-Knoten als Kommunikationsmittel ge-(/miss-?)braucht.

    Christoph

    1. Falls die in Safari eingesetzte JS Engine hinreichend gut optimiert, tritt das beschriebene Problem erst gar nicht auf: Da in den inneren Funktionen kein eval() vorkommt, muss die Closure keine Referenz auf die komplette lexikale Umgebung der äußeren Funktion bereithalten, sondern nur Referenzen auf die tatsächlich benötigten Objekte.

      Dazu müsste sie ja zur »Compile-Time« alle Identifier der inneren Funktion prüfen, ob darin Variablen sind, die nur in der äußeren Funktion notiert sind.

      Das kann die Engine aber aus Prinzip nicht, sonst würde folgendes nicht funktionieren:

      (function () {  
      	var lokVar = "Hallo";  
      	window.onload = function closure () {  
      		alert(eval("lokVar"));  
      	}  
      })();
      

      Das geht aber im JavaScriptCore/Squirelfish, was mich zur Annahme führt, dass tatsächlich das gesamte Variable Object gespeichert bleibt.

      http://www.jibbering.com/faq/faq_notes/closures.html#clScCh sagt, dass das auch vorgeschrieben ist:

      »Function objects created with function declarations or function expressions have the scope chain of the execution context in which they are created assigned to their internal [[scope]] property. (...) Inner function declarations and expressions result in function objects being created within the execution context of a function so they get more elaborate scope chains. (...) Variable instantiation for that new execution context results in the creation of a function object that corresponds with the inner function definition and the [[scope]] property of that function object is assigned the value of the scope from the execution context in which it was created.«

      D.h. eine Closure hat folgende Scope Chain:

      1. Activation/Variable Object der inneren Funktion
      2. Activation/Variable Object der äußeren Funktion
      3. Globales Objekt

      Konsequenz daraus:
      http://www.jibbering.com/faq/faq_notes/closures.html#clFrmC

      »... the function object now referred to by globalVar was created with a [[scope]] property referring to a scope chain containing the Activation/Variable object belonging to the execution context in which it was created (and the global object). Now the Activation/Variable object -cannot be garbage collected [! --molily] either as the execution of the function object referred to by globalVar will need to add the whole scope chain [!] from its [[scope]] property to the scope of the execution context created for each call to it.«

      Mathias

      1. Moin.

        Beachte:

        »» [...] Da in den inneren Funktionen kein eval() vorkommt [...]

        Das kann die Engine aber aus Prinzip nicht, sonst würde folgendes nicht funktionieren:

        (function () {

        var lokVar = "Hallo";
        window.onload = function closure () {

        !!!       alert(eval("lokVar"));     !!!

        }
        })();

          
        Wie gesagt, nur falls in den inneren Funktionen `eval()` \_nicht\_ vorkommt, kann die Engine entscheiden, welche Variablen referenziert werden und nur diese bereithalten. Dies ist kein Widerspruch zum Standard, da nach außen hin kein Unterschied bemerkbar ist.  
          
        Ob eine der aktuellen Engines das tatsächlich implementiert, ist eine andere Frage. Da inzwischen einige der JS Engines JIT kompilieren, halte ich es für nicht allzu abwegig, dass inzwischen auch komplexere Optimierungen durchgeführt werden.  
          
        Christoph
        
        1. nur falls in den inneren Funktionen eval() _nicht_ vorkommt, kann die Engine entscheiden

          Das ändert nichts an dem Problem, dass ich zur Compile-Time nicht weiß, wie bestimmte Identifier zur Run-Time auflösen werden.

          Ich kann bei der Kompilation alle Identifier heraussuchen und schauen, welche lokalen Variablen deklariert werden. Für die Identifier, die sich nicht mit den letzteren überschneiden, muss die Scope Chain jenseits des inneren Variable Objects abgearbeitet werden. Bei der Ausführung könnte ich dann nur diese konservieren und die restlichen sofort per GC entsorgen. Soweit hast du Recht.

          Bei der eval-Frage müsste ich zur Compile-Zeit nach allen Referenzen auf eval suchen. Das wird m.W. erst in ECMAScript 5 ermöglicht, weil es da eine Unterscheidung zwischen Direktaufruf über den Identifier »eval« und dem indirektem Aufruf gibt (z.B. var myEval = eval; myEval('code')). Vgl. 10.4.2 und 15.1.2.1.1 vom Final Draft. So eine Unterscheidung haben manche Browser schon eingebaut:

          var e = eval;  
          (function () {  
          	var lokVar = "lokale Variable";  
          	window.onload = function closure () {  
          		var u = eval;  
          		alert(eval('typeof lokVar') + ' ' + u('typeof lokVar') + ' ' + e('typeof lokVar'));  
          	}  
          })();
          

          Ergibt je nach Browser
          string string string
          oder
          string undefined undefined

          Letzteres ist, soweit ich das verstehe, ECMAScript-5-konform - da Squirelfish letzteres ausgibt, muss ich dir Recht gegen, dass er eine solche Optimierung prinzipiell vornehmen könnte.

          Vielleicht senden wir mal so eine Frage an die Webkit-Dev-Liste? Das würde mich doch interessieren!

          Mathias