JavaScript のメモリ管理は自動的に行われ、私たちには見えません。私たちはプリミティブ、オブジェクト、関数を作成します...すべてメモリを必要とします。
何かが必要なくなったらどうなるのでしょうか? JavaScript エンジンはどのようにそれを検出し、クリーンアップするのでしょうか?
JavaScript におけるメモリ管理の主な概念は到達可能性です。
簡単に言えば、「到達可能な」値とは、何らかの方法でアクセス可能または使用可能な値です。これらはメモリに保存されることが保証されています。
本質的に到達可能な値の基本セットがあり、明白な理由により削除できません。
例えば:
これらの値はrootと呼ばれます。
現在実行中の関数、そのローカル変数およびパラメーター。
ネストされた呼び出しの現在のチェーン上の他の関数、そのローカル変数およびパラメーター。
グローバル変数。
(他にも内部的なものもあります)
他の値は、ルートから参照または参照のチェーンによって到達可能であれば、到達可能であるとみなされます。
たとえば、グローバル変数にオブジェクトがあり、そのオブジェクトに別のオブジェクトを参照するプロパティがある場合、そのオブジェクトは到達可能であるとみなされます。そして、それが参照するものにも到達可能です。詳細な例は後述します。
JavaScript エンジンには、ガベージ コレクターと呼ばれるバックグラウンド プロセスがあります。すべてのオブジェクトを監視し、到達不能になったオブジェクトを削除します。
最も単純な例を次に示します。
// ユーザーはオブジェクトへの参照を持っています ユーザー = { にします 名前:「ジョン」 };
ここで、矢印はオブジェクト参照を示しています。グローバル変数"user"
オブジェクト{name: "John"}
を参照します (簡潔にするために、これを John と呼びます)。 John の"name"
プロパティにはプリミティブが格納されているため、オブジェクト内にペイントされます。
user
の値が上書きされると、参照は失われます。
ユーザー = null;
現在、ジョンは連絡が取れなくなりました。それにアクセスする方法も参照することもできません。ガベージ コレクターはデータをジャンクし、メモリを解放します。
ここで、 user
からadmin
に参照をコピーしたと想像してみましょう。
// ユーザーはオブジェクトへの参照を持っています ユーザー = { にします 名前:「ジョン」 }; 管理者 = ユーザーとします。
ここで同じことをすると:
ユーザー = null;
…その後、オブジェクトはadmin
グローバル変数を介してアクセスできるため、メモリ内に留まる必要があります。 admin
も上書きすると、削除できます。
次に、より複雑な例を示します。家族:
関数marry(男性、女性) { 女性.夫 = 男性; 男性.妻 = 女性; 戻る { 父:男、 母:女性 } } 家族 = 結婚しましょう({ 名前:「ジョン」 }、{ 名前:「アン」 });
関数marry
2つのオブジェクトに相互参照を与えることによって「結合」し、それらの両方を含む新しいオブジェクトを返します。
結果として得られるメモリ構造は次のとおりです。
現時点では、すべてのオブジェクトに到達可能です。
次に、2 つの参照を削除しましょう。
家族と父親を削除します。 家族、母親、夫を削除します。
すべてのオブジェクトには引き続きアクセスできるため、これら 2 つの参照のうち 1 つだけを削除するだけでは十分ではありません。
しかし、両方を削除すると、John には受信参照がなくなったことがわかります。
発信参照は関係ありません。受信したものだけがオブジェクトを到達可能にできます。そのため、ジョンは現在アクセス不能になり、アクセス不能になったすべてのデータとともにメモリから削除されます。
ガベージコレクション後:
相互リンクされたオブジェクトの島全体が到達不能になり、メモリから削除される可能性があります。
ソースオブジェクトは上記と同じです。それから:
家族 = null;
メモリ内の画像は次のようになります。
この例は、到達可能性の概念がいかに重要であるかを示しています。
ジョンとアンが依然としてリンクされていることは明らかであり、両方とも受信参照を持っています。しかしそれだけでは十分ではありません。
以前の"family"
オブジェクトはルートからリンクが解除されており、参照がなくなったので、島全体が到達不能になり、削除されます。
基本的なガベージ コレクション アルゴリズムは「マーク アンド スイープ」と呼ばれます。
次の「ガベージ コレクション」手順が定期的に実行されます。
ガベージ コレクターはルートを取得し、それらを「マーク」します (記憶します)。
次に、それらからのすべての参照を訪問して「マーク」します。
次に、マークされたオブジェクトを訪問し、その参照をマークします。訪問したすべてのオブジェクトは記憶されるため、今後同じオブジェクトを 2 回訪問することはありません。
…そして、到達可能なすべての (ルートからの) 参照がアクセスされるまで続きます。
マークされたオブジェクトを除くすべてのオブジェクトが削除されます。
たとえば、オブジェクト構造が次のようになっているとします。
右側に「到達不可能な島」がはっきりと見えます。ここで、「マーク アンド スイープ」ガベージ コレクターがこれをどのように処理するかを見てみましょう。
最初のステップではルートにマークを付けます。
次に、それらの参照をたどり、参照されたオブジェクトにマークを付けます。
…可能な限り、さらなる参考文献に従い続けます。
プロセス中にアクセスできなかったオブジェクトは到達不能とみなされ、削除されます。
このプロセスは、巨大なバケツの絵の具を根元からこぼし、すべての参照を流れて、到達可能なすべてのオブジェクトにマークを付けるようなものだと想像することもできます。その後、マークのないものは削除されます。
これがガベージ コレクションの仕組みの概念です。 JavaScript エンジンは、コードの実行に遅延を生じさせずに実行を高速化するために多くの最適化を適用します。
最適化の一部:
世代別コレクション– オブジェクトは「新しいもの」と「古いもの」の 2 つのセットに分割されます。一般的なコードでは、多くのオブジェクトの寿命は短く、出現して仕事をし、すぐに消滅するため、新しいオブジェクトを追跡し、その場合はメモリをクリアするのが合理的です。十分に長く生き残ったものは「古く」なり、検査される頻度が減ります。
増分コレクション– 多数のオブジェクトがあり、オブジェクト セット全体を一度に調べてマークしようとすると、時間がかかり、実行に目に見える遅延が発生する可能性があります。したがって、エンジンは既存のオブジェクトのセット全体を複数の部分に分割します。そして、これらの部分を順番にクリアしていきます。全体的なガベージ コレクションではなく、多数の小さなガベージ コレクションがあります。そのためには、変更を追跡するためにそれらの間で追加の簿記が必要になりますが、大きな遅延ではなく、小さな遅延が多く発生します。
アイドル時コレクション– ガベージ コレクターは、実行への影響を軽減するために、CPU がアイドル状態のときにのみ実行を試みます。
ガベージ コレクション アルゴリズムには他の最適化やフレーバーが存在します。ここでそれらについて説明したいのですが、エンジンごとに実装される調整やテクニックが異なるため、差し控える必要があります。そして、さらに重要なことは、エンジンが開発されるにつれて状況は変化するため、実際の必要性なしに「事前に」深く勉強することはおそらく価値がありません。もちろん、純粋に興味がある場合を除き、以下にいくつかのリンクがあります。
知っておくべき主な事項:
ガベージコレクションは自動的に実行されます。私たちはそれを強制したり阻止したりすることはできません。
オブジェクトは、アクセス可能な間はメモリ内に保持されます。
参照されることは、(ルートから) 到達可能であることと同じではありません。上記の例で見たように、相互リンクされたオブジェクトのパックは、全体として到達不能になる可能性があります。
最新のエンジンは、ガベージ コレクションの高度なアルゴリズムを実装しています。
一般書籍『ガベージ コレクション ハンドブック: 自動メモリ管理の技術』 (R. Jones et al) では、その一部について説明しています。
低レベルのプログラミングに精通している場合は、V8 のガベージ コレクターの詳細については、「V8 のツアー: ガベージ コレクション」の記事を参照してください。
V8 ブログでは、メモリ管理の変更に関する記事も随時公開しています。当然のことながら、ガベージ コレクションについてさらに詳しく知るには、V8 の内部全般について学び、V8 エンジニアの 1 人として働いていた Vyacheslav Egorov のブログを読んで準備することをお勧めします。私が「V8」と言っているのは、インターネット上の記事で最もよく取り上げられているからです。他のエンジンの場合、多くのアプローチは似ていますが、ガベージ コレクションは多くの点で異なります。
低レベルの最適化が必要な場合には、エンジンに関する深い知識が役立ちます。この言語に慣れてきたら、次のステップとして計画するのが賢明でしょう。