ヒープ スナップショットを記録する

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

[メモリ] > [プロファイル] > [ヒープ スナップショット] を使用してヒープ スナップショットを記録し、メモリリークを見つける方法について説明します。

ヒープ プロファイラには、ページの JavaScript オブジェクトと関連する DOM ノードのメモリ分布が表示されます。このツールを使用して、JS ヒープのスナップショットの取得、メモリグラフの分析、スナップショットの比較、メモリリークの検出を行います。詳細については、オブジェクト保持ツリーをご覧ください。

スナップショットを撮影

ヒープ スナップショットを取得するには:

  1. プロファイリングするページで DevTools を開き、[メモリ] パネルに移動します。
  2. [ ヒープ スナップショット] プロファイリング タイプを選択し、JavaScript VM インスタンスを選択して、[スナップショットを取得] をクリックします。

選択したプロファイリング タイプと JavaScript VM インスタンス。

[メモリ] パネルでスナップショットが読み込まれて解析されると、[ヒープ スナップショット] セクションのスナップショット タイトルの下に、到達可能な JavaScript オブジェクトの合計サイズが表示されます。

到達可能なオブジェクトの合計サイズ。

スナップショットには、グローバル オブジェクトから到達可能なメモリグラフ内のオブジェクトのみが表示されます。スナップショットの取得は、必ずガベージ コレクションから始まります。

分散された Item オブジェクトのヒープ スナップショット。

スナップショットを消去する

すべてのスナップショットを削除するには、[] [すべてのプロファイルをクリア] をクリックします。

すべてのプロファイルを消去します。

スナップショットを表示

目的に応じてさまざまな視点からスナップショットを検査するには、上部のプルダウン メニューからいずれかのビューを選択します。

表示 コンテンツ 目的
概要 コンストラクタ名別にグループ化されたオブジェクト。 このビューでは、型に基づいてオブジェクトとそのメモリ使用量を追跡します。DOM リークの追跡に役立ちます。
比較 2 つのスナップショットの違い。 このビューでは、操作前後の 2 つ(またはそれ以上)のスナップショットを比較します。解放されたメモリと参照カウントの差を調べて、メモリリークの存在と原因を確認します。
封じ込め ヒープの内容 オブジェクト構造に適したビューが提供されるため、グローバル名前空間 "(window)" で参照されるオブジェクトを分析し、保持内容を調べるのに役立ちます。このビューでは、クロージャを分析して下位レベルのオブジェクトまで踏み込んで調査します。
統計情報 メモリ割り当ての円グラフ コード、文字列、JS 配列、型付き配列、システム オブジェクトに割り当てられたメモリ部分の相対サイズを確認します。

上部にあるプルダウン メニューから選択した概要ビュー。

概要ビュー

最初に、[Summary] ビューでヒープスナップショットが開き、コンストラクタが列に表示されます。コンストラクタを展開すると、コンストラクタがインスタンス化したオブジェクトを確認できます。

コンストラクタが展開された概要ビュー。

関連性のないコンストラクタを除外するには、[概要] ビューの上部にある [クラスフィルタ] に、検査する名前を入力します。

コンストラクタ名の横にある数字は、コンストラクタで作成されたオブジェクトの合計数を示します。[概要] ビューには、次の列も表示されます。

  • [距離] には、ノードへの単純な最短パスにおけるルートまでの距離が表示されます。
  • [浅いサイズ] には、特定のコンストラクタによって作成されたすべてのオブジェクトの浅いサイズの合計が表示されます。浅いサイズとは、オブジェクト自体が保持するメモリサイズです。一般に、配列と文字列の浅いサイズは大きくなります。オブジェクト サイズもご覧ください。
  • [Retained size] には、同じオブジェクトセットの最大保持サイズが表示されます。保持サイズは、オブジェクトを削除してその依存オブジェクトに到達できなくすることで解放できるメモリのサイズです。オブジェクト サイズもご覧ください。

コンストラクタを展開すると、[概要] ビューにそのすべてのインスタンスが表示されます。各インスタンスの浅いサイズと保持サイズの内訳が対応する列に表示されます。@ 文字の後の数字はオブジェクトの一意の ID です。オブジェクト単位でヒープのスナップショットを比較できます。

コンストラクタ フィルタ

[概要] ビューでは、メモリ使用量が非効率的な一般的なケースに基づいてコンストラクタをフィルタできます。

これらのフィルタを使用するには、アクションバーの右端にあるプルダウン メニューから次のいずれかのオプションを選択します。

  • すべてのオブジェクト: 現在のスナップショットでキャプチャされたすべてのオブジェクト。デフォルトで設定されます。
  • スナップショット 1 の前に割り振られたオブジェクト: 最初のスナップショットが作成される前に作成され、メモリ内に残ったオブジェクト。
  • スナップショット 1 とスナップショット 2 の間に割り振られたオブジェクト: 最新のスナップショットと前のスナップショットのオブジェクトの差異を確認します。新しいスナップショットを作成するたびに、このフィルタの増分値がプルダウン リストに追加されます。
  • 重複する文字列: メモリに複数回保存されている文字列値。
  • デタッチされたノードで保持されているオブジェクト: デタッチされた DOM ノードが参照しているため存続しているオブジェクト。
  • DevTools コンソールで保持されているオブジェクト: DevTools コンソールから評価または操作されたため、メモリに保持されているオブジェクト。

[概要] の特別なエントリ

[Summary] ビューでは、コンストラクタによるグループ化に加えて、次の項目でオブジェクトをグループ化することもできます。

  • ArrayObject などの組み込み関数。
  • タグ(<div><a><img> など)でグループ化された HTML 要素。
  • コードで定義した関数。
  • コンストラクタに基づかない特別なカテゴリ。

コンストラクタ エントリ。

(array)

このカテゴリには、JavaScript で表示されるオブジェクトに直接対応していない、さまざまな内部配列のようなオブジェクトが含まれます。

たとえば、JavaScript Array オブジェクトの内容は、サイズ変更を容易にするために、(object elements)[] という名前のセカンダリ内部オブジェクトに保存されます。同様に、JavaScript オブジェクトの名前付きプロパティは、(array) カテゴリにも表示される (object properties)[] という名前のセカンダリ内部オブジェクトに格納されることがよくあります。

(compiled code)

このカテゴリには、JavaScript または WebAssembly で定義された関数を実行するために V8 が必要とする内部データが含まれます。各関数は、小規模で低速なものから大規模で高速なものまで、さまざまな方法で表すことができます。

V8 は、このカテゴリのメモリ使用量を自動的に管理します。関数が何度も実行されると、V8 はより多くのメモリをその関数に使用して、より速く実行できるようにします。関数がしばらく実行されていない場合、V8 はその関数の内部データを消去することがあります。

(concatenated string)

V8 が 2 つの文字列を連結する場合(JavaScript の + 演算子など)、結果を内部で「連結文字列」(ロープ データ構造)として表現することがあります。

V8 は、2 つのソース文字列のすべての文字を新しい文字列にコピーするのではなく、2 つのソース文字列を指す firstsecond という内部フィールドを持つ小さなオブジェクトを割り振ります。これにより、V8 は時間とメモリを節約できます。JavaScript コードの観点から、これらは単なる通常の文字列であり、他の文字列と同じように動作します。

InternalNode

このカテゴリは、Blink で定義された C++ オブジェクトなど、V8 の外部で割り当てられたオブジェクトを表します。

C++ クラス名を表示するには、Chrome for Testing を使用して次の操作を行います。

  1. DevTools を開き [設定] > [試験運用版] > [ヒープ スナップショットで内部を公開するオプションを表示] をオンにします。
  2. [メモリ] パネルを開き、[ ヒープ スナップショット] を選択し、[ 内部データを表示する(実装に固有の追加情報を含む)] をオンにします。
  3. InternalNode が大量のメモリを保持する原因となった問題を再現します。
  4. ヒープ スナップショットを取得します。このスナップショットでは、オブジェクトに InternalNode ではなく C++ クラス名が付けられています。
(object shape)

V8 の高速プロパティで説明されているように、V8 は非表示クラス(またはシェイプ)を追跡し、同じプロパティを持つ複数のオブジェクトを同じ順序で効率的に表現できるようにします。このカテゴリには、system / Map という名前の非表示クラス(JavaScript の Map とは無関係)と関連データが含まれます。

(sliced string)

JavaScript コードが String.prototype.substring() を呼び出す場合など、V8 がサブ文字列を取得する必要がある場合、V8 は元の文字列から関連する文字をすべてコピーするのではなく、スライスされた文字列オブジェクトを割り当てることを選択する場合があります。この新しいオブジェクトには、元の文字列へのポインタが含まれ、元の文字列のどの範囲の文字を使用するかを記述します。

JavaScript コードの観点から、これらは単なる通常の文字列であり、他の文字列と同様に動作します。スライスされた文字列が大量のメモリを保持している場合、プログラムが Issue 2869 をトリガーしている可能性があります。この場合は、スライスされた文字列を「フラット化」するための意図的な手順を踏むことで、問題を解決できる可能性があります。

system / Context

system / Context タイプの内部オブジェクトには、ネストされた関数がアクセスできる JavaScript スコープであるクロージャーのローカル変数があります。

すべての関数インスタンスには、実行される Context への内部ポインタが含まれているため、これらの変数にアクセスできます。Context オブジェクトは JavaScript から直接は見えませんが、直接制御できます。

(system)

このカテゴリには、(まだ)意味のある方法で分類されていないさまざまな内部オブジェクトが含まれています。

比較ビュー

[比較] ビューでは、複数のスナップショットを相互に比較して、リークされたオブジェクトを見つけることができます。たとえば、ドキュメントを開いて閉じるといった操作を実行して元に戻す場合、余分なオブジェクトが残らない必要があります。

特定のオペレーションでリークが作成されないことを確認するには:

  1. オペレーションを実行する前にヒープ スナップショットを取得します。
  2. オペレーションを実行します。つまり、リークの原因と思われる何らかの方法でページを操作します。
  3. 逆オペレーションを実行します。つまり、反対の操作を数回繰り返します。
  4. 2 つ目のヒープ スナップショットを取得し、ビューを [比較] に変更して、スナップショット 1 と比較します。

[比較] ビューには、2 つのスナップショットの違いが表示されます。集計エントリを展開すると、オブジェクトの追加されたインスタンスと削除されたインスタンスが表示されます。

スナップショット 1 との比較。

Containment ビュー

Containment ビューは、アプリケーションのオブジェクト構造の「鳥瞰図」です。関数クロージャの内部を見て、JavaScript オブジェクトを構成する VM の内部オブジェクトを観察し、アプリケーションが非常に下位レベルで使用するメモリ量を把握できます。

ビューには複数のエントリ ポイントがあります。

  • DOMWindow オブジェクト。JavaScript コードのグローバル オブジェクト。
  • GC ルート。VM のガベージ コレクタで使用される GC ルート。GC ルートは、組み込みオブジェクトのマップ、シンボルテーブル、VM スレッドスタック、コンパイル キャッシュ、ハンドルスコープ、グローバル ハンドルから構成されます。
  • ネイティブ オブジェクト。自動化を可能にするために、JavaScript 仮想マシン内に「プッシュされる」ブラウザ オブジェクトです。たとえば、DOM ノードや CSS ルールなどがあります。

Containment ビュー。

[Retainers] セクション

[メモリ] パネルの下部にある [保持] セクションには、ビューで選択したオブジェクトを参照するオブジェクトが表示されます。[統計情報] 以外のビューで別のオブジェクトを選択すると、[メモリ] パネルの [保持] セクションが更新されます。

[Retainers] セクション。

この例では、選択した文字列は Item インスタンスの x プロパティによって保持されます。

リテーナーを無視する

保持子を非表示にして、選択したオブジェクトを保持している他のオブジェクトがあるかどうかを確認できます。このオプションを使用すると、コードからこの保持ツールを削除してからヒープ スナップショットを再取得する必要がなくなります。

プルダウン メニューの [この保持者を無視] オプション。

保持子を非表示にするには、保持子を右クリックして [この保持子を無視] を選択します。無視された保持子は、[距離] 列に ignored とマークされます。すべての保持を無視しないようにするには、上部にあるアクションバーで [ 無視された保持を復元] をクリックします。

特定のオブジェクトを見つける

ガベージ コレクションが行われたヒープ内でオブジェクトを見つけるには、Ctrl+F を使用してオブジェクト ID を入力します。

関数に名前を付けてクロージャを区別する

スナップショット内のクロージャを区別できるように、関数に名前を付けると非常に役立ちます。

たとえば、次のコードは名前付きの関数を使用していません。

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function() { // this is NOT a named function
    return largeStr;
  };

  return lC;
}

上記の例を以下のようにします。

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function lC() { // this IS a named function
    return largeStr;
  };

  return lC;
}

クロージャー内の名前付き関数。

DOM リークの検出

ヒープ プロファイラには、ブラウザのネイティブ オブジェクト(DOM ノード、CSS ルール)と JavaScript オブジェクト間の双方向の依存関係を反映する機能があります。これにより、デタッチされ、忘れられた DOM サブツリーがあるために発生する、目に見えないリークを検出するのに役立ちます。

DOM リークは考えている以上に大きくなることがあります。次の例をご覧ください。#tree はいつガベージ コレクションされますか?

  var select = document.querySelector;
  var treeRef = select("#tree");
  var leafRef = select("#leaf");
  var body = select("body");

  body.removeChild(treeRef);

  //#tree can't be GC yet due to treeRef
  treeRef = null;

  //#tree can't be GC yet due to indirect
  //reference from leafRef

  leafRef = null;
  //#NOW #tree can be garbage collected

#leaf は親(parentNode)への参照を保持し、再帰的に #tree まで参照するため、leafRef が Null フィールドになる場合のみ、#tree の下のツリー全体が GC の候補になります。

DOM サブツリー