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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

[メモリ] > [プロファイル] > [ヒープ スナップショット] を使用してヒープ スナップショットを記録し、メモリリークを検出する方法を学びます。

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

スナップショットを撮影

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

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

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

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

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

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

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

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

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

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

スナップショットを表示

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

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

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

概要ビュー

最初は、[概要] ビューにヒープスナップショットが開き、コンストラクタが列に表示されます。コンストラクタの名前は、オブジェクトを作成した JavaScript 関数の名前にちなんで付けられます。単純なオブジェクトの名前は、そのオブジェクトに含まれるプロパティに基づいています。一部の名前は特別なエントリです。すべてのオブジェクトは、まず名前でグループ化され、次に、そのオブジェクトがソースファイルのどの行に存在するかでグループ化されます(例: source-file.js:line-number)。

グループ化されたコンストラクタを展開すると、インスタンス化されたオブジェクトを確認できます。

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

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

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

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

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

コンストラクタ フィルタ

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

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

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

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

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

  • 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] ビューは、アプリのオブジェクト構造の「鳥瞰図」です。これにより、関数クロージャーの中身を調べたり、JavaScript オブジェクトを構成する VM 内部オブジェクトを観察したり、アプリケーションが使用するメモリ量を非常に低いレベルで把握したりできます。

このビューには、次のようなエントリ ポイントがあります。

  • DOMWindow オブジェクト。JavaScript コードのグローバル オブジェクト。
  • GC ルート。VM のガベージ コレクタで使用される GC ルート。GC ルートには、組み込みオブジェクトマップ、シンボルテーブル、VM スレッドスタック、コンパイル キャッシュ、ハンドル スコープ、グローバル ハンドルが含まれます。
  • ネイティブ オブジェクト。ブラウザ オブジェクトが JavaScript 仮想マシン内に「push」され、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 サブツリー