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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

[メモリ] > [プロファイル] > [ヒープ スナップショット] でヒープ スナップショットを記録し、メモリリークを見つける方法を学習します。

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

スナップショットを撮影

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

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

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

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

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

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

分散したアイテム オブジェクトのヒープ スナップショット。

スナップショットを消去

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

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

スナップショットを表示

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

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

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

概要表示

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

展開されたコンストラクタを含む [Summary] ビュー。

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

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

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

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

[Summary] の特別なエントリ

Summary ビューでは、コンストラクタによるグループ化に加えて、次の方法でオブジェクトもグループ化します。

  • 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 データ構造)として内部的に表現することもできます。

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

InternalNode

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

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

  1. DevTools を開き設定の [設定] > [テスト] > check_box [ヒープ スナップショットで内部構造を公開するオプションを表示する] をオンにします。
  2. [メモリ] パネルを開き、[radio_button_checked ヒープ スナップショット] を選択して、radio_button_checkedExpose internals (contains additionalimplementation-specific details)」をオンにします。
  3. InternalNode が大量のメモリを保持する原因となっていた問題を再現します。
  4. ヒープのスナップショットを取得します。このスナップショットでは、オブジェクトのクラス名は InternalNode ではなく C++ です。
(object shape)

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

(sliced string)

V8 で部分文字列を取得する必要がある場合(JavaScript コードが String.prototype.substring() を呼び出す場合など)、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 ビュー

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

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

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

Containment ビュー。

リテイナー・セクション

[Memory] パネルの下部にある [Retainers] セクションには、ビューで選択したオブジェクトをポイントするオブジェクトが表示されます。

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

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

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

収集されたヒープ内のオブジェクトを見つけるには、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 サブツリー