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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

Memory を使用してヒープ スナップショットを記録する方法を学ぶ >プロフィール >ヒープ スナップショットでメモリリークを見つける。

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

スナップショットを撮影

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

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

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

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

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

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

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

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

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

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

スナップショットを表示

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

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

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

概要ビュー

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

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

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

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

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

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

コンストラクタ フィルタ

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

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

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

概要の特別なエントリ

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

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

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

(array)

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

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

(compiled code)

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

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

(concatenated string)

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

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

InternalNode

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

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

  1. DevTools を開き [Settings] をオンにします >テスト > ヒープ スナップショットの内部を公開するオプションを表示
  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)

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

比較ビュー

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

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

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

[Comparison] ビューには、2 つのスナップショットの違いが表示されます。合計ウィンドウを表示する場合 追加および削除されたオブジェクト インスタンスが表示されます。

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

包含ビュー

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

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

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

Containment ビュー。

リテイナーのセクション

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

[Retainers] セクション。

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

リテーナーを無視

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

「このリテーナーを無視する」オプションを選択します。

リテーナーを非表示にするには、右クリックして [このリテーナーを無視] を選択します。無視されたリテーナーには、[Distance] 列で 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 が無効化されると、#tree の下のツリー全体が GC の候補になります。

DOM サブツリー