読者です 読者をやめる 読者になる 読者になる

破棄されたブログ

このブログは破棄されました。

「JavaScript アプリケーションのメモリー・リークを理解する」を読んだのでメモ

今更ながら読んだ。思い違いとかもあったりで色々とメモ。
JavaScript アプリケーションのメモリー・リークを理解する

まずは定義から

メモリーリークというのは、不要になったオブジェクトが存在し続けること。

JavaScript におけるオブジェクトのライフサイクル

 +------------------+   
 | オブジェクト作成 |
 +------------------+   
           |
           v
+----------------------+
| メモリの自動割り当て |
+----------------------+
           |<---------+
           v          | 破棄されるまでループ
   +---------------+  |  
   | GC による評価 |--+
   +---------------+
           | 参照カウントがゼロ OR 唯一の参照が互いに循環参照のみ
           v 
        +------+
        | 破棄 |
        +------+

循環参照について備考

循環参照先のオブジェクトが別のオブジェクトからの参照を持っている場合、
破棄の対象にはならない。
つまり、循環参照している「各々の」オブジェクトの参照が循環参照のみである必要がある。

A, B ともに破棄の対象       A, B ともに破棄の対象外
     +---------+           +---------+
     | objectA |           | objectA |
     +---------+           +---------+
         ^ |                   ^ |
         | v                   | v
     +---------+           +---------+   +---------+
     | objectB |           | objectB |<--| objectC |
     +---------+           +---------+   +---------+

正常に破棄されるシンプルな例

assets/scripts/leaker.js
var Leaker = function(){};
(function(Leaker) {
  if (typeof Leaker !== "function") {
    return;
  }

  Leaker.prototype = {
    init: function() {}
  };
})(Leaker);
assets/scripts/main.js
// this should be in global namespace
var leak = {};

$(document).ready(function(){
  $("#start_button").click(function(){
    if (!leak) {
      return;
    }
    console.log("start clicked");
    leak = new Leaker();
    leak.init();
  });

  $("#destroy_button").click(function(){
    console.log("destroy clicked");
    leak = null;
  });
});
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Understand memory leaks in JavaScript applications</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
    <script src="assets/scripts/leaker.js"></script>
    <script src="assets/scripts/main.js"></script>
  </head>
  <body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
  </body>
</html>

Closure を使った破棄されないハズの例

なぜか破棄される。

assets/scripts/leaker.js
var Leaker = function(){};
(function(Leaker) {
  if (typeof Leaker !== "function") {
    return;
  }

  Leaker.prototype = {
    cnt: 0,
    init: function() {
      this._interval = null;
      this.start();
    },
    start: function() {
      var self = this;
      this._interval = setInterval(function(){
        self.onInterval(self);
      }, 100);
    },
    destroy: function() {
      if (this._interval !== null) {
        clearInterval(this._interval);
      }
    },
    onInterval: function(self) {
      console.log(++self.cnt);
      console.log(++self.cnt);
    }
  };
})(Leaker);
assets/scripts/main.js
// this should be in global namespace
var leak = {};

$(document).ready(function(){
  $("#start_button").click(function(){
    if (!leak) {
      return;
    }
    console.log("start clicked");
    leak = new Leaker();
    leak.init();
  });

  $("#destroy_button").click(function(){
    console.log("destroy clicked");
    // leak.destroy();
    leak = null;
  });
});
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Understand memory leaks in JavaScript applications</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
    <script src="assets/scripts/leaker.js"></script>
    <script src="assets/scripts/main.js"></script>
  </head>
  <body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
  </body>
</html>
メモリヒープのスナップショット
+ leak in Window
| + global in @11637
|   + builtins in Window @6437
|   + blobal_receiver in @11637
|   + builtins in @11637
|   + builtins in @11637
+ self in function() @108355
  + [167] in (Global handles) @27
    + [9] in (GC roots) @3

これは回収されないと書かれているけど、なぜか回収される。 (Google Chrom 26.0.1410.64 m)
したがって、Destroy ボタンを押した後には Leaker オブジェクトのインスタンスカウントがゼロになる。
それと、インスタンスが生き残っていることを確認するために、cnt プロパティを追加して、 onInterval メソッドでインクリメントするようにした結果、インスタンスが生き残っていることの確認もできた。
Leaker でなくて別のツリーに移動するんだろうか。確認はできてない。

本来であれば、Destroy ボタンを押しても Leaker オブジェクトの参照カウントがゼロにならず、メモリリークを起こす。
これは、イベントハンドラのコールバックに指定されたクロージャがインスタンスへの参照を保持しているため。

destroy メソッド

インスタンスへの参照を破棄する際に呼び出すことで、適切にオブジェクトをクリーンナップできるメソッド

destroy が適切でない場合
  • オブジェクト A はクリーンナップメソッド destroy を持つ
  • 変数 a は、A のインスタンス
  • オブジェクト B, C は a への参照を持つ

B で a を破棄する際に、destroy() を呼び出してクリーンナップをした後から、C がクリーンナップされていない前提で a を使用する可能性がある。

destroy が適切な場合

1 つのオブジェクトに対して明確な所有者が 1 人いて、その所有者がそのオブジェクトのライフサイクルに責任を持つ場合

JavaScript アプリケーションのメモリー・リークを理解する

コンソール

コンソールでオブジェクトをログ出力すると、ログクリアをするまで、参照が保持され続ける。

循環参照

assets/scripts/leaker.js
var Leaker = function(){};
(function(Leaker) {
  if (typeof Leaker !== "function") {
    return;
  }

  Leaker.prototype = {
    init: function(name, parent, registry) {
      this._name     = name;
      this._parent   = parent;
      this._child    = null;
      this._registry = registry;
      this.createChildren();
      this.registerCallback();
    },
    createChildren: function() {
      if (this._parent !== null) {
        return;
      }
      this._child = new Leaker();
      this._child.init("leaker 2", this, this._registry);
    },
    registerCallback: function() {
      this._registry.add(this);
    },
    destroy: function() {
      if (this._child) {
        // this._child.destroy();
      }
      this._registry.remove(this);
    }
  };
})(Leaker);
assets/scripts/main.js
// this should be in global namespace
var leak
  , registry;

$(document).ready(function(){
  registry = new Registry();
  registry.init();

  $("#start_button").click(function(){
    console.log("start clicked");
    var leakExists = !(
      window["leak"] === null || window["leak"] === undefined
      );
    if (leakExists) {
      return;
    }

    leak = new Leaker();
    leak.init("leaker 1", null, registry);
  });

  $("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
  });
});
assets/scripts/regitry.js
var Registry = function() {};

(function(Registry){
  Registry.prototype = {
    init: function() {
      this._subscriber = [];
    },
    add: function(subscriber) {
      if (this._subscriber.indexOf(subscriber) >= 0) {
        // alredy registered
        return;
      }
      this._subscriber.push(subscriber);
    },
    remove: function(subscriber) {
      if (this._subscriber.indexOf(subscriber) < 0) {
        // not currently registered
        return;
      }
      this._subscriber.splice(
        this._subscriber.indexOf(subscriber), 1
      );
    }
  };
})(Registry);
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Understand memory leaks in JavaScript applications</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
    <script src="assets/scripts/regitry.js"></script>
    <script src="assets/scripts/leaker.js"></script>
    <script src="assets/scripts/main.js"></script>
  </head>
  <body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
  </body>
</html>

Start ボタンを押すと次のコードが実行されて、下図の様な状態になる

leak = new Leaker();
leak.init("leaker 1", null, registry);
                      +--------+
          +-----------| window |-----------+
          |           +--------+           |
          | leak                           |
          v      _registry                 |
      +------+<---------------------+      | registry
      | leak |--------------------+ |      |
      +------+   _subscriber[1]   v |      |
        | ^                  +----------+  |
 _child | | _parent          | registry |<-+
        v |                  +----------+
     +-------+   _subscriber[0]   | ^
     | child |<-------------------+ |
     +-------+----------------------+
                  _registry

* 参照 -> 被参照

leak.destroy() を呼び出すと、
registry かの参照が切れる。

leak.destroy();
                +--------+
          +-----| window |------+
          |     +--------+      |
          |                     |
          v                     |
      +------+                  |
      | leak |---------+        |
      +------+         v        |
        | ^       +----------+  |
        | |       | registry |<-+
        v |       +----------+
     +-------+         | ^
     | child |<--------+ |
     +-------+-----------+

leak を null にすると、window から leak への参照が切れる。
ただし、window からの参照がきれても、
child からの参照が生き残っているため、破棄されない。

leak = null;
                +--------+
                | window |------+
                +--------+      |
	v this will not be collected.
      +------+                  |
      | leak |---------+        |
      +------+         v        |
        | ^       +----------+  |
        | |       | registry |<-+
        v |       +----------+
     +-------+         | ^
     | child |<--------+ |
     +-------+-----------+


そこで、leak と child を独立した循環参照状態にさせる。
この状態にするために、leak, child 両方への registry からの参照を切る。

leak._child.destroy();
leak.destroy();
                +--------+
          +-----| window |------+
          |     +--------+      |
          |                     |
          v                     |
      +------+                  |
      | leak |---------+        |
      +------+         v        |
        | ^       +----------+  |
        | |       | registry |<-+
        v |       +----------+
     +-------+         ^
     | child |---------+
     +-------+

leak, child の参照カウントはそれぞれ 1 で、
循環参照のみになるので、破棄の対象になる

leak = null;
                +--------+
                | window |------+
                +--------+      |
                                |
                                |
      +------+                  |
      | leak |---------+        |
      +------+         v        |
        | ^       +----------+  |
        | |       | registry |<-+
        v |       +----------+
     +-------+         ^
     | child |---------+
     +-------+

破棄される。

                +--------+
                | window |------+
                +--------+      |
                                |
                                |
                                |
                                |
                                |
                  +----------+  |
                  | registry |<-+
                  +----------+
広告を非表示にする