Matti Mäkitalo: Garbage Collector bei Listenern, wenn EventTarget entfernt wird

Hi,

ich habe eine kleine Javascript-Applikation geschrieben.
Dabei habe ich den Applikationscode erstmal in eine "Seiten-Klasse" gekapselt, welche für die Darstellung der einzelnen Teil-Screens verantwortlich ist. Neben diesen Screen-Funktionen (welche im wesentlichen nur Templates aufrufen und in die Seite einfügen und dann etwaige Buttons u.a. mit Actions versehen) gibt es eben die Listener-Funktionen.

Nun ist mir folgendes Szenario einigermaßen verständlich (korrigiert mich bitte, wenn das falsch ist).
angenommen ich habe eine Klasse "Screens", welche u.a. Methoden bereithält, welche per listen an Elemente meines DOM gebunden werden.

Wenn ich sämtliche Referenzen auf mein Screens-Objekt entferne, dann wird es trotzdem nicht entsorgt, da durch das binding der listener-Funktionen diese noch aufgerufen werden könnten. Um mein Objekt also tatsächlich zu entsorgen, muss ich alle Listener entfernen.

Jetzt andersherum gedacht:
momentan ist diese eine Screens-Klasse für eine ganze Gruppe an Sichten verantwortlich (allerdings bin ich am Überlegen, pro einzelne Sicht eine einzelne Klasse zu schreiben, bin mir aber über Vor- und Nachteile noch nicht ganz im Klaren).
Das Objekt hat eine Referenz auf meinen Element-Knoten, in dem ich den Inhalt darstellen will und ersetzt beim Anzeigen einfach dessen Inhalt durch den Neuen.
Wird dadurch auch der Garbage Collector für den alten Inhalt aufgerufen? Oder bleibt dieser im Speicher, da noch durch die Listener Referenzen dafür vorliegen?

Dazugehörende Frage:
gibt es für den Firefox oder einen anderen Browser eine einfache Möglichkeit, den Speicherverbrauch der aktuellen Seite bzw. von JavaScript-Objekten anzuzeigen? Oder vielleicht sogar, was insgesamt so im Speicher rumfliegt?

Bis die Tage,
Matti

  1. Wenn ich sämtliche Referenzen auf mein Screens-Objekt entferne, dann wird es trotzdem nicht entsorgt, da durch das binding der listener-Funktionen diese noch aufgerufen werden könnten. Um mein Objekt also tatsächlich zu entsorgen, muss ich alle Listener entfernen.

    Richtig. Sonst bleiben die Listener vom GC verschont. Wenn diese noch per Closure Daten einschließen (z.B. das zugehörige Objekt durch Binding), dann bleiben auch diese im Speicher.

    Das Objekt hat eine Referenz auf meinen Element-Knoten, in dem ich den Inhalt darstellen will und ersetzt beim Anzeigen einfach dessen Inhalt durch den Neuen.
    Wird dadurch auch der Garbage Collector für den alten Inhalt aufgerufen? Oder bleibt dieser im Speicher, da noch durch die Listener Referenzen dafür vorliegen?

    Wenn du ein Teil des DOMs auswechselst (appendChild/removeChild, innerHTML usw.), so zerstörst du damit i.d.R. auch die alten DOM-Objekte.
    Verweisen diese DOM-Objekte auf Handlerfunktionen, so wird diese Referenz mit zerstört.
    Verweist kein weiteres Objekt mehr auf die Funktionen, so ist die Anzahl der Referenzen 0 und sie können vom GC abgeräumt werden.

    Der GC läuft periodisch, daher kann das tatsächliche Löschen aus dem Speicher etwas später erfolgen.

    Verwendest du jQuery für Event-Handling und DOM-Operationen, so werden Event-Listener immer entfernt, bevor das DOM-Element aus dem Baum entfernt wird. Das ist die besonders sichere und speicherschonende, wenn auch weniger performante Methode.

    gibt es für den Firefox oder einen anderen Browser eine einfache Möglichkeit, den Speicherverbrauch der aktuellen Seite bzw. von JavaScript-Objekten anzuzeigen? Oder vielleicht sogar, was insgesamt so im Speicher rumfliegt?

    Schau dir einmal den Profiles-Tab des Web Inspectors an (Safari, Chrome). Im Heap Snapshot kannst du genau sehen, womit der Speicher gefüllt ist.

    Mathias

    1. Hi,

      Wenn du ein Teil des DOMs auswechselst (appendChild/removeChild, innerHTML usw.), so zerstörst du damit i.d.R. auch die alten DOM-Objekte.
      Verweisen diese DOM-Objekte auf Handlerfunktionen, so wird diese Referenz mit zerstört.
      Verweist kein weiteres Objekt mehr auf die Funktionen, so ist die Anzahl der Referenzen 0 und sie können vom GC abgeräumt werden.

      Mein Screen-Objekt bleibt eigentlich während der gesamten Dauer der Applikation im Speicher, das sollte eigentlich nicht zerstört werden.
      Mir ging es eher um die alten, nicht mehr benutzten DOM-Objekte. Es wäre sehr ärgerlich, wenn diese einfach weiter im Speicher bleiben würden und somit nach und nach diesen zukleistern, obwohl die Elemente nicht mehr angezeigt werden (und auch nicht irgendwie angedacht ist, diese zu recyceln: im Falle eines Wiederaufrufs einer Sicht erzeuge ich diese einfach neu).

      Verwendest du jQuery für Event-Handling und DOM-Operationen, so werden Event-Listener immer entfernt, bevor das DOM-Element aus dem Baum entfernt wird. Das ist die besonders sichere und speicherschonende, wenn auch weniger performante Methode.

      Ich teste momentan mit der Closure-Library herum und lese in diesem Zusammenhang Closure: The Definitive Guide. Bolin (der Autor) hat nun an deutlich größeren Projekten gearbeitet als meine Applikation je erreichen wird, aber da es durchaus sein kann, dass meine App tage- wenn nicht wochenlang durchläuft, will ich wenigstens sichergehen, dass solche einfachen Fälle an unnötigem Speicherverbrauch nicht auftreten.

      Closure enthält extra Funktionen, um einfacher mit dem Release von Listenern zurechtzukommen. Bolin schreibt auch ein eigenes Kapitel über die goog.Disposable-Klasse.

      Dadurch bin ich ehrlich gesagt ein wenig aufgeschreckt. Und ich mache mir ein wenig Sorgen, dass durch die ganzen verwendeten Closures viel zu viel Zeugs im Speicher gehalten wird, weil es noch in irgendwelchen Scopes von Listenern rumgammelt -.-.

      gibt es für den Firefox oder einen anderen Browser eine einfache Möglichkeit, den Speicherverbrauch der aktuellen Seite bzw. von JavaScript-Objekten anzuzeigen? Oder vielleicht sogar, was insgesamt so im Speicher rumfliegt?

      Schau dir einmal den Profiles-Tab des Web Inspectors an (Safari, Chrome). Im Heap Snapshot kannst du genau sehen, womit der Speicher gefüllt ist.

      Danke.

      Bis die Tage,
      Matti

      1. Mein Screen-Objekt bleibt eigentlich während der gesamten Dauer der Applikation im Speicher, das sollte eigentlich nicht zerstört werden.
        Mir ging es eher um die alten, nicht mehr benutzten DOM-Objekte. Es wäre sehr ärgerlich, wenn diese einfach weiter im Speicher bleiben würden

        Ich glaube, du verstehst Referenzen falsch, sonst würdest du dir diese Frage nicht stellen. ;) Ein Objekt b verweist auf ein Objekt a. Das sorgt dafür, das a solange erhalten bleibt, wie diese (oder andere) Referenzen existieren. Beispiel:

        var a = {};
        var b = { ref : a };
        a = null;

        a kann hier nicht abgeräumt werden, weil es referenziert wird.

        Wenn ich b lösche oder die Referenz entferne, so verweist nichts mehr auf a; es kann abgeräumt werden.

        Dein Fall ist nun: Elementobjekt verweist auf Funktionsobjekt.

        var handler = function () {};
        var el = document.createElement('p');
        el.onclick = handler;
        document.body.appendChild(el);

        Nicht der Handler verweist auf das Element. Es ist keine zirkuläre Referenz.

        Wenn du das Element jetzt aus dem DOM entfernst, dann kann es auch abgeräumt werden. Die einzige Referenz darauf war die des Elternelements, welches ins Dokument eingehängt ist.

        Mathias

        1. Hi,

          Dein Fall ist nun: Elementobjekt verweist auf Funktionsobjekt.

          var handler = function () {};
          var el = document.createElement('p');
          el.onclick = handler;
          document.body.appendChild(el);

          Nicht der Handler verweist auf das Element. Es ist keine zirkuläre Referenz.

          Wenn du das Element jetzt aus dem DOM entfernst, dann kann es auch abgeräumt werden. Die einzige Referenz darauf war die des Elternelements, welches ins Dokument eingehängt ist.

          Ja, das war mir soweit klar und ist verständlich.
          Dachte, es könnte noch sein, dass diese Listener-Verknüpfung noch irgendwo global gespeichert wird.

          Bis die Tage,
          Matti