SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

CoffeeScriptによるモダンなWebアプリケーション開発

CoffeeScriptベストプラクティス集
ブラウザ向けJavaScript編(3)

CoffeeScriptによるモダンなWebアプリケーション開発 第9回

  • X ポスト
  • このエントリーをはてなブックマークに追加

メモリリークを見つける

 メモリリークとは、使用されたメモリが解放されないまま残り、メモリの空き容量がどんどん減っていく症状のことを指します。ページ内で使用されたメモリは通常、他のページを読み込んだり、ブラウザを終了すると自動的に解放されます。Ajaxを利用すると、ページ遷移を行わずに部分的にコンテンツを読み込んで表示できますが、1つのページを長時間開き続けることが多くなります。ゲームのようなコンテンツも同じページを長時間開き続けることが多いでしょう。このようにユーザーが長時間操作を続けてもページ遷移がないケースでは、正しくメモリ解放を行わずに新しいメモリを使い続けると、メモリが足りなくなりブラウザがだんだん重くなっていくことがあります。この現象がメモリリークです。

 ブラウザがどのくらいメモリを使用しているかは、Windowsであればタスクマネージャ、Macであればアクティビティモニタで確認できます。また、Chromeに付属しているタスクマネージャを使うと、ブラウザのタブごとにメモリ使用量を確認できます(図8)。

図8 タスクマネージャでタブごとのメモリ使用量を見る
図8 タスクマネージャでタブごとのメモリ使用量を見る

 キャッシュのためなど意図してメモリを確保している場合はよいのですが、意図せずに大量のメモリが消費されていく状況は対処する必要があります。とくに、ユーザーの操作に比例してメモリの消費量が大きくなる場合、1回や2回操作しただけでは何ともなくても、数十回、数百回操作を繰り返した場合に問題が起きることがあります。ユーザーが長時間滞在することを意図したページでは、不要なメモリリークが起きていないかどうか注意してチェックしましょう。

 より詳しくメモリリークを見つけるための方法を紹介します。

タイムラインでメモリリークを確認する

 調査対象のページをChromeで開き、デベロッパーツールを起動してTimelineパネルを開きます。Memoryタブをクリックし、下のバーにある丸いアイコンのRecordボタンをクリックするとボタンが赤に変わって記録が始まり、メモリ使用量のグラフが描画されていきます。この状態のままページに戻り、ボタンを押すなどの操作をすると、デベロッパーツールのグラフが変化してメモリ使用量の推移がわかります。

図9 メモリリークしていない場合
図9 メモリリークしていない場合

 図9のようにところどころ崖のように落ちている箇所は、ガベージコレクタが起動してメモリが解放されたことを表しています。操作を繰り返してもメモリ使用量が一定の範囲を保っていれば、メモリリークは起きていません。

図10 メモリリークしている場合
図10 メモリリークしている場合

 一方、図10のようにメモリの使用量が増え続けている場合、メモリリークが発生しています。ガベージコレクタが起動してもメモリが解放できないため、崖ができずに上がり続けています。

 再びRecordボタンを押すと記録が停止します。調査を繰り返し、どの操作がメモリリークに直結しているのかを推測します。メモリリークに結びつく操作が推測できたら、今度はメモリ内部の状況を詳しく見ていきます。

調査対象のHTML

 今回はリスト4のHTMLをプロファイラで調査してみます。action()関数の中で大量のdiv要素を作成し、それらをグローバル変数divsに追加しています。

 JavaScriptでは、どこからの参照も持たなくなったオブジェクトはガベージコレクタによって回収され、空きメモリとして他の用途に使えるようになります。参照が1つでも残っているオブジェクトはガベージコレクタに回収されずにメモリ上に残ります。

 リスト4ではaction()の実行が終わってもdivsからdivへの参照が残るため、作成されたdivはすべてメモリ上に残ります。

[リスト4]メモリリークするHTML
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Leak</title>
  <script>
    var divs = [];
    function action() {
      for (var i = 0; i < 10000; i++) {
        var div = document.createElement('div');
        // 100バイトの文字列を付加
        div.myString = new Array(101).join('1');
        divs.push(div);
      }
    }
  </script>
</head>
<body>
  <button id="actionButton" onclick="action()">実行</button>
</body>
</html>

操作の前後の状況を比較する

 Chromeでリスト4のHTMLを開き、デベロッパーツールを起動してProfilesパネルを開きます。Take Heap Snapshotを選択してStartボタンを押します。スナップショットの作成が始まり、しばらく待つと左サイドバーにSnapshot 1が現れます。これで操作前のスナップショットが取れました。次に、ページに戻って「実行」ボタンを押します。再びデベロッパーツールに戻り、下の黒丸ボタンを押してスナップショットを取ります。しばらく待つとSnapshot 2が現れます。これが操作後のスナップショットです。

 Snapshot 2をクリックし、下のバーのSummaryと表示されているボタンをクリックしてComparisonを選択し、しばらく待つとデータが表示されます(図11)。このComparisonビューは2つのスナップショット間の差分を表しています。それぞれの列の意味は、「# New」は新しく作成された数、「# Deleted」は削除された数、その右のデルタ列は# Newと# Deletedの差分です。Alloc. Sizeは確保されたメモリサイズ、Freed Sizeは解放されたメモリサイズ、そして一番右のデルタ列はAlloc. SizeとFreed Sizeの差分を表しています。

図11 Comparisonビューでスナップショットどうしを比較する
図11 Comparisonビューでスナップショットどうしを比較する

 一番右のデルタ列をクリックして降順にソートすると、(string)、つまり文字列が1.08MB増加したことがわかります。(string)に付いている右三角のマークをクリックして展開すると個々の文字列が現れ、そのうちの一つをクリックすると、選択した文字列がどのオブジェクトによって参照されているのかが下の「Object's retaining tree」に表示されます(図12)。

図12 オブジェクトの参照元を調べる
図12 オブジェクトの参照元を調べる

 ここでは、文字列がHTMLDivElementから参照されていることがわかります。赤の背景色は、documentに属していないDOM要素であることを表します。HTMLDivElementの階層を展開すると、このHTMLDivElementはArray: @24711という配列から参照されており、Array: @24711はDOMWindow、つまりwindowオブジェクトから参照されていることがわかります。

 このArray: @24711の正体を突き止めるため、下のComparisonボタンをクリックしてContainmentビューに移動します(図13)。

図13 Containmentビュー
図13 Containmentビュー

 Containmentビューではオブジェクト構造の全景を見ることができます。DOMWindowの階層を展開してArray: @24711を探すと、divsという変数名が表示されています(図14)。このdivsの中身をクリアして参照を切ればメモリリークを防げることがわかります。

図14 Array: @24711の変数名はdivsである
図14 Array: @24711の変数名はdivsである

クロージャによるメモリ消費

 クロージャを使う場合、意図せずサイズの大きな変数がメモリを占拠することがないよう注意してください。

 例えば、以下のコードではaction()関数内のローカル変数としてbigStringを作成していますが、そのbigStringはクロージャ内で使用されています。このクロージャはグローバル変数であるglobalObjに保持されるため、action()関数の実行後もbigStringが保持されます。クロージャは便利な反面、どのメモリが保持されているかわかりづらくなるため気をつけてください。

[リスト5]クロージャによるメモリ消費
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Leak demo</title>
  <script>
    var globalObj;
    function action() {
      // 10MBの文字列を作成
      var bigString = new Array(10485761).join('1');
      globalObj = function() { return bigString };
    }
  </script>
</head>
<body>
  <button id="actionButton" onclick="action()">実行</button>
</body>
</html>

次のページ
古いInternet Explorerでの検証

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
CoffeeScriptによるモダンなWebアプリケーション開発連載記事一覧

もっと読む

この記事の著者

飯塚 直(イイヅカ ナオ)

1984年東京都生まれ。 高校時代に趣味でPerlやJavaを使ってプログラミングを始める。 慶応大学湘南藤沢キャンパス卒業後、共同通信社にてニュースサイトの開発などを担当。 その後、面白法人カヤックにてソーシャルゲームの開発などを手がける。 2012年現在、カヤックを退社し個人として活動し...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/6537 2012/05/14 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング