Don't forget the Chrome Dev Summit, starting Monday at 10:00am (Pacific) and streaming live on YouTube. Schedule.

メモリに関する用語

このセクションでは、メモリ分析に使用される一般的な用語について説明します。このセクションは、さまざまな言語のさまざまなメモリ プロファイリング ツールに当てはめることができます。

ここに記載する用語と表記は、Chrome DevTools のヒープ プロファイラを参考にしています。Java、.NET などのメモリ プロファイラの使用経験がある方は復習としてお読みください。

オブジェクトのサイズ

メモリを、プリミティブ型(数値や文字列など)とオブジェクト(連想配列)を持つグラフと考えます。視覚的には、以下のように多数の相互接続点を持つグラフとして表現できます。

メモリの視覚表現

オブジェクトは、以下の 2 つの方法でメモリを保持します。

  • オブジェクト自体によって直接保持される。

  • 他のオブジェクトへの参照を保持することで暗黙に保持する。このようなオブジェクトは、ガベージ コレクター(GC)によって自動的に破棄されません。

DevTools のヒープ プロファイラ([Profiles] にあるメモリの問題を調査するツール)を使用する場合は、いくつか異なる列の情報を調べることになります。ハイライトしている 2 つの列 [Shallow Size] と [Retained Size] は何を表すのでしょう。

Shallow Size と Retained Size

浅いサイズ

これは、オブジェクト自体が保持しているメモリのサイズです。

一般的な JavaScript オブジェクトは、説明用とイミディエイト値格納用にメモリを確保します。通常、浅いサイズが意味を持つのは配列と文字列のみです。ただし、文字列や外部配列は、多くの場合、レンダラーのメモリにメイン ストレージを確保します。JavaScript ヒープに公開されるのは、小さなラッパー オブジェクトのみです。

レンダラーのメモリとは、調査対象のページをレンダリングするプロセスが使用するすべてのメモリを指します。つまり、ネイティブ メモリ、ページの JS ヒープメモリ、ページによって開始される専用ワーカーすべての JS ヒープメモリの総和です。とは言っても、小さなオブジェクトでも、他のオブジェクトが自動ガベージ コレクション プロセスによって破棄されないようにすることによって、間接的に大量のメモリを保持することになります。

保持サイズ

これは、オブジェクト自体とその依存オブジェクトが削除されると解放されるメモリのサイズです。依存オブジェクトは、GC ルートから到達できないように設定されています。

GC ルートは、ハンドルで構成されます。ハンドルは、ネイティブ コードから V8 外部の JavaScript オブジェクトへの参照を作成するときに、(ローカルまたはグローバルに)作成されます。そのようなハンドルはすべて、ヒープ スナップショット内の [GC roots] の [Handle scope] と [GC roots] の [Global handles] にあります。このドキュメントでブラウザの実装について詳しく触れずにハンドルについて説明すると、混乱を招く恐れがあります。このドキュメントでは GC ルートについてもハンドルについても知っておく必要はありません。

内部 GC ルートはたくさんありますが、ユーザーにとって重要なものはほとんどありません。アプリケーションから考えると、以下の種類のルートがあります。

  • ウィンドウ グローバル オブジェクト (各 iframe 内)。ヒープ スナップショットには距離フィールドがあります。このフィールドは、ウィンドウからの最短保持パス上にあるプロパティ参照の数を示します。

  • ドキュメント全体を調べることによって到達できるすべてのネイティブ DOM ノードで構成されるドキュメント DOM ツリー。JS ラッパーを持たない DOM ノードもありますが、JS ラッパーがある場合、ドキュメントが有効な間はラッパーも有効です。

  • オブジェクトは、デバッガー コンテキストと DevTools コンソールによって保持されることがあります(コンソールの評価後など)。コンソールをクリアし、デバッガーでブレークポイントがアクティブになっていない状態で、ヒープ スナップショットを作成します。

メモリのグラフはルートから始まります。ルートになり得るのは、ブラウザーの window オブジェクトや Node.js モジュールの Global オブジェクトなどです。このルート オブジェクトに対して GC が行われる方法を制御することはできません。

ルート オブジェクトは制御不能

ルートから到達できないオブジェクトが GC の対象になります。

注: Shallow size 列と Retained size 列では、データがバイト単位で表現されます。

オブジェクトの保持ツリー

ヒープは相互に連結されたオブジェクトのネットワークです。数学的には、このような構造を「グラフ」またはメモリグラフと呼びます。グラフは「エッジ」によって連結される「ノード」から作成されます。どちらもラベルが指定されます。

  • ノード(「オブジェクト」)のラベルは、ビルドに使用した Constructor 関数の名前を使用して付けられます。
  • エッジのラベルは、プロパティの名前を使用して付けられます。

ヒープ プロファイラを使用してプロファイルを記録する方法を参照してください。以下のヒープ プロファイラの記録でハイライトされている箇所には、距離が含まれています。これは、GC ルートからの距離です。同じ型のほとんどすべてのオブジェクトは等距離になります。距離の値が大きい場合、調査する価値があります。

ルートからの距離

ドミネーター

各オブジェクトに含まれるドミネーターは 1 つだけなので、ドミネーターのオブジェクトはツリー構造になります。オブジェクトのドミネーターには、それが支配するオブジェクトへの直接参照がない場合があります。つまり、ドミネーターのツリーはグラフのスパニング ツリーではありません。

以下の図について考えます。

  • ノード 1 はノード 2 を支配しています
  • ノード 2 はノード 3、4、6 を支配しています
  • ノード 3 はノード 5 を支配しています
  • ノード 5 はノード 8 を支配しています
  • ノード 6 はノード 7 を支配しています

ドミネーターのツリー構造

以下の例では、ノード #3#10 のドミネーターですが、GC から #10 へのすべての単純なパスには #7 も存在します。そのため、オブジェクト B がルートからオブジェクト A へのすべての単純なパスに存在する場合、オブジェクト B はオブジェクト A のドミネーターになります。

アニメーションを用いたドミネーターの図

V8 の仕様

メモリをプロファイリングするときは、ヒープ スナップショットによって特定の方法で表示される理由を理解すると役に立ちます。このセクションでは、V8 JavaScript 仮想マシン(V8 VM または VM)に具体的に対応するメモリ関連のトピックについて説明します。

JavaScript オブジェクトの表現

以下の 3 つのプリミティブ型があります。

  • 数値(例、3.14159..)
  • ブール値(true または false)
  • 文字列値(例: 「Werner Heisenberg」)

これらの型は他の値を参照できないため、必ずリーフノードまたは終端ノードになります。

数値は以下のいずれかで格納できます。

  • 小整数(SMI)と呼ばれる 31 ビットのイミディエイト整数値。
  • ヒープ数値と呼ばれるヒープ オブジェクト。ヒープ数値は、double 型など、SMI 形式に合わない値を格納する場合、またはプロパティの設定など、値をボックス化する必要がある場合に使用されます。

文字列は以下のいずれかに格納できます。

  • VM ヒープ
  • レンダラーのメモリの外部。ラッパー オブジェクトは、VM ヒープにコピーされるのではなく、ウェブから受け取るスクリプト ソースやその他のコンテンツが格納されるような外部ストレージにアクセスするために作成、使用されます。

新しい JavaScript オブジェクト用のメモリは、専用の JavaScript ヒープ(VM ヒープ)から割り当てられます。これらのオブジェクトは V8 のガベージ コレクターによって管理されるため、少なくとも 1 つの強い参照があれば、コレクションが行われずに残ります。

ネイティブ オブジェクトとは、JavaScript ヒープに含まれていないすべてのオブジェクトを指します。ヒープ オブジェクトとは異なり、ネイティブ オブジェクトは有効期間中、V8 のガベージ コレクターによって管理されません。JavaScript ラッパー オブジェクトを使用して、JavaScript からのみアクセスできます。

cons 文字列とは、その後の連結を目的として格納される文字列のペアで構成されるオブジェクトです。文字列は連結の結果になります。「cons 文字列」のコンテンツの連結は、必要な場合にのみ行われます。連結後の文字列の部分文字列を用意しておく必要がある場合などがその例です。

たとえば、ab を連結する場合、連結の結果を表す文字列を(a, b)と表現します。その後、この結果に d を連結する場合は、別の cons 文字列として((a, b), d)と表現します。

配列 - 配列は数値キーを持つオブジェクトです。大量のデータを格納するために、V8 VM で広く使用されています。ディクショナリなどに使用されるキーと値のペアのセットには、配列を利用します。

一般的な JavaScript オブジェクトは、以下の 2 つの配列型のいずれかの格納に使用されます。

  • 名前付きプロパティ
  • 数値要素

プロパティの数が非常に少ない場合は、JavaScript オブジェクト自体の内部に格納できます。

マップ - オブジェクトの型とそのレイアウトを表すオブジェクトです。たとえば、マップはプロパティに高速にアクセスするために、暗黙のオブジェクト階層を表すために使用されます。

オブジェクト グループ

それぞれのネイティブ オブジェクト グループは、相互参照を保持するオブジェクトで構成されます。たとえば、DOM サブツリーについて考えます。このサブツリーでは、各ノードに親へのリンクと、次の子とその次の兄弟へのリンクを含み、相互に接続されるグラフを形成します。ネイティブ オブジェクトは JavaScript ヒープでは表現されません。そのため、サイズはゼロになります。代わりに、ラッパー オブジェクトが作成されます。

各ラッパー オブジェクトは、コマンドをリダイレクトするために、対応するネイティブ オブジェクトへの参照を保持します。今度は、オブジェクト グループがラッパー オブジェクトを保持します。ただし、ラッパーを参照しなくなったオブジェクト グループは GC によって解放されるため、コレクションを実行できないサイクルが生み出されることがなくなります。ですが、1 つでもラッパーの解放を忘れると、グループ全体と関連付けられたラッパーが保持されます。