banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

v8エンジンのガベージコレクションの原理

ガベージコレクション、メモリの最適化#

私たちは、JS がデータを保存するためにヒープとスタックを使用することを知っています(メモリの割り当て)

  • プリミティブデータ型、コードの実行時にスタックフレームの位置をスタックスペースに保存します
  • 参照型データはヒープスペースに保存されます

しかし、一部のデータは使用後に不要になり、ガベージデータになります。これらが回収されない場合、メモリ使用量が増え続けます。
一般的なガベージコレクションには、手動回収自動回収の 2 つの戦略があります。

C/C++ のような言語では、手動回収戦略が使用され、コードでメモリの割り当てと解放方法を決定する必要があります。一方、Javascript/Java/Python などの言語では、ガベージコレクタがメモリを自動的に解放しますが、メモリ管理を無視することはありません(特に JS の場合、自動ガベージコレクションによって混乱しやすく、メモリ管理を無視することがあります)

1. スタックメモリの回収#

まず、呼び出しスタック内のデータがどのように回収されるかを見てみましょう。実行コンテキストから始めます

関数の実行コンテキストが作成され、スタックに入ると、プリミティブデータはスタックに割り当てられ、参照型データはヒープに割り当てられます。

同時に、現在の実行状態を記録するポインタ(ESP)があり、現在の実行コンテキスト A を実行します。

関数 A の呼び出しが完了し、スタックから出ると、ESP は次の実行コンテキスト B に移動します。この移動操作は、前の実行コンテキスト A を破棄するプロセスです。実行コンテキスト A はまだスタックメモリに保存されていますが、無効なメモリです。次に他の関数を呼び出すと、このメモリを直接上書きして他の関数の実行コンテキストを保存するために使用します。

image

したがって、関数の実行が終了すると、JS エンジンは ESP を下に移動させてその関数の実行コンテキストを破棄します。

2. ヒープメモリの回収#

スタック内の実行コンテキストが破棄された後、ヒープに保存されているオブジェクトのいくつかはどのように処理されますか?それにはガベージコレクタが必要です

2.1 世代別収集と主なプロセス#

V8 では、ヒープを新生代と老生代の 2 つの領域に分けます

  • 新生代:寿命の短いオブジェクトを保存する -> マイナーガベージコレクタ Minor GC
  • 老生代:寿命の長いオブジェクトを保存する -> メジャーガベージコレクタ Major GC

ガベージコレクタには統一された実行フローがあります

  • オブジェクトのマーク
  • アクティブオブジェクト:使用中のオブジェクト
  • 非アクティブオブジェクト:ガベージコレクションが可能なオブジェクト
  • 非アクティブオブジェクトが占めるメモリの回収
  • メモリの整理:メモリの断片を整理し、連続したスペースを確保し、後続の大きな連続メモリの割り当てを容易にする

2.2 マイナーガベージコレクタ#

image

新しく追加されたオブジェクトはオブジェクト領域に追加され、オブジェクト領域が一杯になるとガベージコレクションを実行する必要があります

オブジェクト領域のガベージをマーク -> アクティブなオブジェクトを空き領域にコピー -> 順番に並べ替え -> 空き領域にガベージフラグメントがない -> オブジェクト領域と空き領域の概念が反転 -> 無限に再利用

マイナーガベージコレクタは、監視オブジェクトがいっぱいになるとガベージコレクションを実行します。同時に、マイナーガベージコレクタはオブジェクトの昇格ポリシーを採用し、2 回のガベージコレクションを経てもまだ生きているオブジェクトを老生代に移動します。

2.3 メジャーガベージコレクタ#

老生代ヒープ内のオブジェクトの特徴

  • 1 つはオブジェクトが大きいこと
  • もう 1 つはオブジェクトの寿命が長いこと

メジャーガベージコレクタは 2 つのアルゴリズムを採用しています

  • マーク - クリア(Mark-Sweep)
  • マーク - コンパクト(Mark-Compact)

JS には最も基本的なガベージコレクションアルゴリズムである参照カウントもありますが、そのアルゴリズムの欠点は循環参照の問題を処理できないことです

ガベージデータをまずマークします。マークフェーズは一組のルート要素から始まり、このルート要素を再帰的にトラバースすることで、このトラバースプロセス中に到達できる要素はアクティブオブジェクトと呼ばれ、到達できない要素はガベージデータと判断できます。

次にクリアします。これがマーク - クリアアルゴリズムです

image

ただし、同じメモリに対してマーク - クリアアルゴリズムを複数回実行すると、大量の非連続メモリフラグメントが生成されます。フラグメントが多すぎると、大きなオブジェクトが十分な連続メモリに割り当てられなくなります。そのため、別のアルゴリズムであるマーク - コンパクトを導入しました。

まず、回収可能なオブジェクトをマークします。ただし、後続のステップでは回収可能なオブジェクトを直接クリアするのではなく、すべての生存オブジェクトを一方に移動し、その後その一方をクリアするだけです。

image

2.4 ガベージコレクションのまとめ#

image

3. ガベージコレクションの効率化#

JavaScript はメインスレッド上で実行されるため、ガベージコレクションアルゴリズムを実行すると、実行中の JavaScript スクリプトが一時停止し、ガベージコレクションが完了するまでスクリプトの実行が再開されません。この動作はフルストップ(Stop-The-World)と呼ばれます。

image

第一に、完全なガベージコレクションタスクを複数の小さなタスクに分割することで、単一の長いガベージコレクションタスクを排除します。
第二に、オブジェクトのマーク、オブジェクトの移動などのタスクをバックグラウンドスレッドで実行することで、メインスレッドの一時停止時間を大幅に減らし、ページの遅延問題を改善し、アニメーション、スクロール、ユーザーインタラクションをよりスムーズにします。

3.1 最適化の方法#

  • 最初の方法は並行ガベージコレクションで、完全なガベージコレクションプロセスを実行する際に、ガベージコレクタは複数の補助スレッドを使用してガベージコレクションを並行して実行します。
  • 2 番目の方法は増分ガベージコレクションで、ガベージコレクタはマーク作業をより小さなチャンクに分割し、メインスレッドのさまざまなタスクの間に挿入して実行します。増分ガベージコレクションを使用すると、ガベージコレクタは完全なガベージコレクションプロセスを一度に実行する必要はありません。各実行はガベージコレクションプロセス全体の一部です。
  • 3 番目の方法は並行ガベージコレクションで、補助スレッドがガベージコレクションを実行する間、メインスレッドも中断されずに自由に実行できるようにします。難点は読み書きロックメカニズムです(ここでは詳しくは触れません)

メジャーガベージコレクタは、すべての方法を総合的に採用しています。マイナーガベージコレクタも一部の方法を採用しています。

image

4. メモリの最適化#

一般的な 3 つのメモリの問題

  • メモリリーク(Memory leak)は、ページのパフォーマンスが悪化する原因となります。
    • JavaScript では、メモリリーク(Memory leak)の主な原因は、他のオブジェクトによって参照されなくなった(効果がない)メモリデータがまだ参照されていることです。
  • メモリの膨張(Memory bloat)は、ページのパフォーマンスが一貫して悪い状態になります。
  • 頻繁なガベージコレクションは、遅延が発生したり頻繁に一時停止したりする原因となります。

4.1 メモリリークの一般的な原因と解決策#

  • グローバル変数:大きなグローバルオブジェクトを設定するのを避ける
  • リスナー、タイマーの参照が適切にクリアされていない
  • クロージャがメモリに残っている:オブジェクトの循環参照が比較的簡単に形成されるため、変数を null に設定して関連を切断します
  • DOM の参照:DOM ノードの参照を保持し続けると、GC が回収されません

これら以外にも、ES6 のWeakMap/WeakSetを使用することが多く、これらはオブジェクトの弱参照を保持します

4.2 メモリリークの識別方法#

  • ブラウザでは、メモリが増加しているかどうかを確認するためにメモリを表示できます
  • Node 環境では、headdump を使用してメモリ情報をキャプチャして分析できます

参考文献#

Orinoco — 新的 V8 垃圾回收器 by Peter Marshal

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。