このセクションでは、メモリ分析で使用される一般的な用語について説明します。この用語は、さまざまな言語のさまざまなメモリ プロファイリング ツールに適用されます。
ここで説明する用語や概念は、Chrome DevTools のヒープ プロファイラに関するものです。Java、.NET、その他の Memory Profiler を扱ったことがあれば、これは復習になります。
オブジェクトのサイズ
メモリは、プリミティブ型(数値や文字列など)とオブジェクト(連想配列)を持つグラフと考えることができます。次のように、相互接続された複数のポイントを持つグラフとして視覚的に表現できます。
オブジェクトは次の 2 つの方法でメモリを保持できます。
- オブジェクト自体によって直接書き込まれます。
- 他のオブジェクトへの参照を保持することで暗黙的に行う。これにより、それらのオブジェクトがガベージ コレクタ(GC)によって自動的に破棄されないようにする。
DevTools のヒープ プロファイラ([Profiles] にあるメモリの問題を調査するツール)を使用すると、数列の情報が表示される可能性があります。特に重要なのは、Shallow Size と Retained Size の 2 つですが、これらは何を表していますか。
シャローサイズ
これは、オブジェクト自体が保持するメモリのサイズです。
一般的な JavaScript オブジェクトでは、説明用と即値を格納するため、いくらかのメモリが予約されています。通常、かなり浅いサイズに設定できるのは配列と文字列のみです。ただし、多くの場合、文字列と外部配列はレンダラのメモリにメイン ストレージがあり、JavaScript ヒープ上で小さなラッパー オブジェクトのみが公開されます。
レンダラメモリは、検査対象のページがレンダリングされるプロセスの全メモリです。つまり、ネイティブ メモリ + ページの JS ヒープメモリ + ページで開始されたすべての専用ワーカーの JS ヒープメモリです。それでも、小さいオブジェクトでも、自動ガベージ コレクション プロセスによって他のオブジェクトが破棄されないようにすることで、大量のメモリを間接的に保持できます。
保持サイズ
これは、オブジェクト自体とともに、GC ルートから到達不能になった依存オブジェクトが削除されると解放されるメモリのサイズです。
GC ルートは、ネイティブ コードから V8 以外の JavaScript オブジェクトを参照する際に、ローカルまたはグローバルに作成されるハンドルで構成されます。このようなハンドルはすべて、ヒープ スナップショット内の [GC roots] > [Handle scope] および [GC roots] > [Global handle] で確認できます。このドキュメントでは、ブラウザ実装の詳細を詳しく説明せずにハンドルについて説明すると、混乱を招く可能性があります。GC ルートとハンドルはどちらも心配する必要はありません。
内部 GC ルートは多数ありますが、そのほとんどはユーザーにとって興味がありません。アプリケーションの観点から見ると、次のような種類のルートがあります。
- ウィンドウ グローバル オブジェクト(各 iframe 内)。ヒープ スナップショットには距離フィールドがあります。これは、ウィンドウからの最短保持パス上にあるプロパティ参照の数です。
- ドキュメントの走査によって到達可能なすべてのネイティブ DOM ノードで構成されるドキュメントの DOM ツリー。その中に JS ラッパーがない場合もありますが、存在する場合は、ドキュメントが存在する間はラッパーも存続します。
- オブジェクトがデバッガ コンテキストと DevTools コンソールによって保持されている場合があります(コンソールの評価後など)。コンソールをクリアし、デバッガーにアクティブなブレークポイントがない状態で、ヒープ スナップショットを作成する。
メモリグラフはルート(ブラウザの window
オブジェクトや Node.js モジュールの Global
オブジェクト)から始まります。このルート オブジェクトの GC 処理は制御できません。
ルートからアクセスできないものはすべて GC を取得します。
オブジェクト保持ツリー
ヒープは、相互接続されたオブジェクトのネットワークです。数学の世界では、この構造をグラフまたはメモリグラフと呼びます。グラフは「エッジ」によって接続されたノードから構成されます。エッジにはラベルが与えられます。
- ノード(またはオブジェクト)は、その構築に使用されたコンストラクタ関数の名前を使用してラベル付けされます。
- エッジは、プロパティの名前を使用してラベル付けされます。
ヒープ プロファイラを使用してプロファイルを記録する方法を確認する。以下の Heap Profiler の記録では、GC ルートからの距離などの注目すべき情報を確認できます。同じタイプのオブジェクトのほぼすべてが同じ距離にあり、少数のオブジェクトがより遠く離れている場合は、調査する価値があります。
ドミネーター
ドミネーター オブジェクトはツリー構造になっています。これは、各オブジェクトにはドミネーターが 1 つだけあるからです。オブジェクトのドミネーターには、それが支配するオブジェクトへの直接参照がない場合があります。つまり、ドミネーターのツリーはグラフのスパニングツリーではありません。
以下の図で説明します。
- ノード 1 がノード 2 を支配しています
- ノード 2 はノード 3、4、6 を支配しています
- Node 3 が Node 5 を支配している
- ノード 5 がノード 8 を支配しています
- Node 6 は Node 7 を支配している
以下の例では、ノード #3
が #10
のドミネーターですが、#7
も GC から #10
へのすべての単純なパスに存在します。したがって、オブジェクト 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 string 内容の結合は必要な場合にのみ行われます。たとえば、結合された文字列の部分文字列を作成する必要がある場合です。
たとえば、a と b を連結すると、連結の結果を表す文字列 (a, b) が得られます。後でその結果と d を連結すると、別の cons 文字列((a, b), d)が得られます。
配列 - 配列は数値キーを持つオブジェクトです。V8 VM では、大量のデータを保存するために広く使用されます。辞書などに使用される Key-Value ペアのセットは、配列でバックアップされます。
一般的な JavaScript オブジェクトは、格納に使用される次の 2 つの配列型のいずれかになります。
- 名前付きプロパティ、
- 数値要素
プロパティの数が非常に少ない場合は、JavaScript オブジェクト自体に内部的に格納できます。
Map - オブジェクトの種類とそのレイアウトを記述するオブジェクト。たとえば、マップは、プロパティにすばやくアクセスするために暗黙的なオブジェクト階層を記述するために使用されます。
オブジェクト グループ
各ネイティブ オブジェクト グループは、相互参照を保持するオブジェクトで構成されます。たとえば、すべてのノードが親へのリンクを持ち、次の子と次の兄弟へのリンクを持つ DOM サブツリーがあるとします。これにより、連結グラフが形成されます。ネイティブ オブジェクトは JavaScript ヒープでは表現されないため、サイズがゼロになります。代わりにラッパー オブジェクトが作成されます。
各ラッパー オブジェクトは、対応するネイティブ オブジェクトへの参照を保持し、コマンドをそのネイティブ オブジェクトにリダイレクトします。次に、オブジェクト グループがラッパー オブジェクトを保持します。ただし、ラッパーが参照されなくなったオブジェクト グループは GC によって解放されるため、収集不能なサイクルが生じることはありません。ただし、1 つのラッパーをリリースし忘れると、グループ全体と関連するラッパーが保持されます。