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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

[Memory] > [Profiles] > [Heap Snapshot] でヒープのスナップショットを記録し、メモリリークを見つける方法を学習します。

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

スナップショットを撮影

ヒープ スナップショットを作成するには:

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

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

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

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

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

散在している Item オブジェクトのヒープ スナップショット。

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

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

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

スナップショットを表示

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

View コンテンツ 目的
概要 コンストラクタ名でグループ化されたオブジェクト。 これを使用して、オブジェクトとそのメモリの使用状況を種類に基づいて探し出します。DOM リークの追跡に便利です。
Comparison 2 つのスナップショットの違い。 オペレーションの前後で 2 つ(または複数)のスナップショットを比較するために使用します。解放されたメモリと参照カウントの差分を調べて、メモリリークの有無と原因を確認します。
封じ込め ヒープの内容 オブジェクト構造をよりわかりやすく表示し、グローバル名前空間(ウィンドウ)で参照されているオブジェクトを分析して、保持されているオブジェクトを見つけるのに役立ちます。クロージャを分析し、低いレベルでオブジェクトを詳しく調べることができます。
統計情報 メモリ割り当ての円グラフ コード、文字列、JS 配列、型付き配列、システム オブジェクトに割り当てられたメモリ部分の実際のサイズを確認できます。

上部のプルダウン メニューで選択された概要ビュー。

概要ビュー

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

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

無関係なコンストラクタを除外するには、[Summary] ビューの上部にある [Class filter] に、検査する名前を入力します。

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

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

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

コンストラクタ フィルタ

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

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

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

概要の特別なエントリ

概要ビューでは、コンストラクタによるグループ化に加えて、次の条件でもオブジェクトがグループ化されます。

  • ArrayObject などの組み込み関数。
  • コード内で定義した関数を使用します。
  • コンストラクタに基づかない特殊なカテゴリ。

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

(array)

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

たとえば、JavaScript Array オブジェクトのコンテンツは、サイズ変更を容易にするために、(object elements)[] というセカンダリ内部オブジェクトに格納されます。同様に、JavaScript オブジェクトの名前付きプロパティは多くの場合、(array) カテゴリにリストされている (object properties)[] というセカンダリ内部オブジェクトに格納されます。

(compiled code)

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

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

(concatenated string)

V8 が JavaScript の + 演算子などを使用して 2 つの文字列を連結する場合、結果を内部で「連結文字列」(Rope データ構造とも呼ばれます)として表現することを選択できます。

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

InternalNode

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

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

  1. DevTools を開いて、[settings] [Settings] > [Experiments] > [check_box] で [ヒープ スナップショットで内部構造を公開するオプションを表示] をオンにします。
  2. [メモリ] パネルを開き、[radio_button_checked] の [ヒープ スナップショット] を選択して、[内部構造の公開(実装固有の詳細情報を含む)] radio_button_checked をオンにします。
  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)

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

比較ビュー

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

特定のオペレーションでリークが発生しないことを確認するには:

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

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

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

包含ビュー

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

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

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

Containment ビュー。

リテイナーのセクション

[Memory] パネルの下部にある [Retainers] セクションには、ビューで選択されたオブジェクトを指すオブジェクトが表示されます。[Statistics] 以外のビューで別のオブジェクトを選択すると、[Memory] パネルの [Retainers] セクションが更新されます。

リテイナーのセクション。

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

リテーナーを無視

保持器を非表示にして、選択したオブジェクトが他のオブジェクトに保持されていないことを確認できます。このオプションを使用すると、最初にこのリテーナーをコードから削除してからヒープのスナップショットを取得し直す必要はありません。

プルダウン メニューの [このリテーナーを無視] オプション。

リテーナーを非表示にするには、右クリックして [このリテーナーを無視] を選択します。無視されたリテーナーには、[Distance] 列で ignored のマークが付きます。すべてのリテーナーを無視しないようにするには、上部のアクションバーにある playlist_remove の [無視したリテーナーを復元] をクリックします。

特定のオブジェクトを検索する

収集したヒープ内のオブジェクトを検索するには、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 サブツリー