2021年12月15日

ガベージコレクション

JavaScriptのメモリ管理は、自動で私たちの目には見えないように行われます。私たちが作るプリミティブ、オブジェクト、関数… それらはすべてメモリを必要とします。

何かがもう必要なくなったとき、何が起こるでしょう?JavaScriptエンジンはどのようにそれを検出し、クリーンアップするのでしょうか?

到達性

JavaScriptのメモリ管理の主要なコンセプトは、到達性 です。

簡単に言えば、「到達可能な」値は、何らかの形でアクセス可能、または使用可能な値です。それらはメモリに格納されることが保証されています。

  1. 本質的に到達可能な値の基本セットがあり、それらは明白な理由により削除されません。

    例:

    • 現在の関数のローカル変数とパラメータ
    • ネストされた呼び出しの、現在のチェーン上の他の関数のローカル変数とパラメータ
    • グローバル変数
    • (他にも幾つか同様に内部のものがあります)

    それらの値は ルート と呼ばれます。

  2. 他の任意の値は、参照または参照のチェーンにより、ルートから到達可能であれば、到達可能とみなされます。

    例えば、ローカル変数にオブジェクトがあった場合、そしてそのオブジェクトが別のオブジェクトの参照をもっていたとすると、そのオブジェクトは到達可能とみなされます。そして、それが参照するものもまた到達可能です。詳しくは後述します。

JavaScriptエンジンにはガベージコレクタと呼ばれるバックグラウンドプロセスがあります。それはすべてのオブジェクトを監視し、到達不可能になったオブジェクトを削除します。

シンプルな例

これは最もシンプルな例です:

// user は オブジェクトへの参照を持っています
let user = {
  name: "John"
};

ここで、矢印はオブジェクトの参照を示しています。グローバル変数 user はオブジェクト {name: "John"} を参照しています(簡略化して John と呼びます)。John の "name" プロパティはプリミティブを格納しているので、オブジェクトの枠内に描かれています。

もし user の値が上書きされると、参照はなくなります。:

user = null;

今、John は到達不能になりました。参照が存在しないため、アクセスする方法はありません。ガベージコレクタはデータを捨て、メモリを解放します。

2つの参照

さて、user の参照を admin にコピーしたとしましょう:

// user は オブジェクトへの参照を持っています
let user = {
  name: "John"
};

let admin = user;

今、先ほどと同じことをしたとすると:

user = null;

…オブジェクトはまだグローバル変数 admin 経由で到達可能なため、メモリ内に存在します。もし admin も上書きしてしまうと、オブジェクトは削除可能となります。

連結されたオブジェクト

これはより複雑な例です。家族(family):

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

関数 marry は与えられた2つのオブジェクトを互いに参照することによって “marry(結婚)” し、それら両方を含む新しいオブジェクトを返します。

結果のメモリ構造:

今のところ、全てのオブジェクトは到達可能です:

ここで、2つの参照を削除してみましょう:

delete family.father;
delete family.mother.husband;

それら2つの参照のうち、1つだけを削除するのでは不十分です。なぜなら全てのオブジェクトはまだ到達可能だからです。

しかし、もし両方を削除すると、John にはもう参照がないことがわかります:

他のオブジェクトへの参照は気にする必要はありません。自身への参照のみがオブジェクトを到達可能にします。したがって、John は今や到達不能で、他の到達不能になったすべてのデータとともにメモリ内から削除されるでしょう。

ガベージコレクションの後:

到達不可能な島

連携されたオブジェクトの島全体が到達不可能になり、メモリから削除される場合があります。

ソースとなるオブジェクトは上記と同じです。 次にこのようにします:

family = null;

すると、メモリの中はこうなります:

この例は、到達可能性の概念がいかに重要であるかを示しています。

John と Ann がまだリンクされているのは明らかで、両方とも他からの参照を持っていることになります。しかしそれだけでは十分ではありません。

以前 "family" であったオブジェクトはルートからのリンクが解除されており、これ以上、他からの参照はないため、島全体が到達不可能になり、削除されることになります。

内部のアルゴリズム

基本のガベージコレクションのアルゴリズムは “マーク・アンド・スイープ” と呼ばれます。

次の “ガベージコレクション” のステップが定期的に実行されます:

  • ガベージコレクタはルートを取得し、 それらを “マーク” (記憶)します。
  • 次に、そこからのすべての参照先を訪れ、“マーク” します。
  • 次に、マークされたオブジェクトへアクセスし、それらの 参照をマークします。訪問されたすべてのオブジェクトは記憶されているので、将来同じオブジェクトへは2度訪問しないようにします。
  • …そしてすべての到達可能な(ルートからの)参照が訪問されるまで続けます。
  • マークされたオブジェクトを除いた、すべてのオブジェクトが削除されます。

例えば、我々のオブジェクト構造はこのように見えます:

右側に明らかに “到達不可能な島” が見えます。今、どのように “マーク・アンド・スイープ” ガベージコレクタがそれを扱うか見てみましょう。

最初のステップではルートをマークします:

次に、それらの参照をマークします:

…そしてそれらの参照も、可能な限り、マークしていきます:

この手順でたどりつけなかったオブジェクトは到達不能とみなされ、削除されます:

このプロセスは、巨大なバケツに入った絵の具を根元からこぼすようなものだと想像することもできます。この絵の具は、すべてのリファレンスを通過して、手の届く範囲のすべてのオブジェクトをマークします。そして、マークされていないものは削除されます。

これが、ガベージコレクションの仕組みのコンセプトです。JavaScriptのエンジンは、実行に影響を与えないように、より速くガベージコレクションを行うための多くの最適化を施します。

これらは最適化のいくつかです:

  • 世代コレクション – オブジェクトは2つのセットに分割されます: “新しいオブジェクト” と “古いオブジェクト” です。多くのオブジェクトは出現し、仕事をするとすぐ不要になります。これらは積極的にクリーンアップされます。長期間生き残ったものは “古い” ものになり、それほど頻繁には検査されません。
  • インクリメンタルコレクション – 多くのオブジェクトがある場合、1度に全てのオブジェクトの集合をマークしようとすると、時間がかかってしまい、実行時に目に見える遅延を引き起こす可能性があります。なので、エンジンはガベージコレクションを小さく分割します。そしてそれらのピースが1つずつ、別々に実行されます。変更を追跡するため、多少の追加の記憶域を必要とはしますが、大きな遅延ではなく多くの小さな遅延になります。
  • アイドルタイムコレクション – ガベージコレクタは、CPUがアイドル状態のときにのみ実行を試み、実行への影響を減らします。

ガベージコレクションアルゴリズムには他にも最適化やバリエーションがあります。ここでそれらも説明したいのですが、止めておかなくてはいけません。なぜなら、エンジンによって、調整方法やテクニックの使い方がバラバラだからです。 そして、さらに重要なのは、エンジンの開発に伴って状況が変化するため、実際に必要がない場合には「先立って」深く進んでいくことはそれほど価値はありません。 もちろん、それが純粋な興味であれば、参照すると良いいくつかのリンクが下にあります。

サマリ

知っておくべきポイント:

  • ガベージコレクションは自動で実行されます。実行を強制したり、防ぐことはできません。
  • オブジェクトは到達可能な間、メモリ上に保持されます。
  • 参照されることは、(ルートから)到達可能であることと同じではありません: 連結されたオブジェクトのパックはそれ全体で到達不可能になる可能性があります。

現代のエンジンはガベージコレクションの高度なアルゴリズムを実装しています。

一般的な本 “ガベージコレクションハンドブック”: 自動メモリ管理の技術(R.Jones et al)は、それらのいくつかをカバーしています。

もしあなたが低レベルのプログラミングに慣れている場合、V8のガベージコレクションに関するより詳細な情報はこの記事A tour of V8: Garbage Collection.にあります。

V8 blog には、メモリ管理の変更に関する記事も随時掲載されています。 ガベージコレクションを学ぶには、V8の内部について一般的に学習し、V8エンジニアの一人として働いていた Vyacheslav Egorov のブログを読むとよいでしょう。 “V8”, それはインターネット上の記事で最もよくカバーされるからです。 他のエンジンでは、多くのアプローチは似ていますが、ガベージコレクションは多くの点で異なります。

低レベルの最適化が必要な場合は、エンジンに関する深い知識が必要です。 あなたが言語に精通した後の次のステップとしてそれを計画することが賢明でしょう。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。