導入

C のような低水準言語は、malloc()free() のような低水準のメモリー管理プリミティブを持ちます。一方、JavaScript の値は、実体 (オブジェクト、文字列など) の生成時に割り当てられ、使用されなくなると「自動的に」開放されます。後者のプロセスはガベージコレクションと呼ばれます。この「自動的に」という言葉は混乱の元で、JavaScript (および他の高水準言語) の開発者に対し、メモリー管理を気にしないという決定をしてもよいという印象を与えます。これは誤りです。

メモリーライフサイクル

プログラミング言語に関係なく、メモリーのライフサイクルはほぼいつも同じです:

  1. 必要なメモリーを割り当てる
  2. 割り当てられたメモリーを使用する (読み込む, 書き込む)
  3. 必要なくなったら、割り当てられたメモリーを開放する

2 に関してはすべての言語で明示的に行われます。1 と 3 は低水準の言語では明示的ですが、JavaScript のような高水準言語では、ほとんどの場合暗黙的に行われます。

JavaScript での割り当て

値の初期化

割り当てでプログラマを悩まさないために、JavaScript では値を宣言したときと同時にメモリーの割り当ても行われます。

var n = 123; // 数値を格納するメモリーが割り当てられます
var s = "azerty"; // 文字列を格納するメモリーが割り当てられます

var o = {
  a: 1,
  b: null
}; // オブジェクトとそれに含まれる値を格納するためのメモリーが割り当てられます

// (オブジェクトの例と同じように) 配列とそれに含まれる値を格納するための
// メモリーが割り当てられます
var a = [1, null, "abra"]; 

function f(a){
  return a + 2;
} // 関数を格納するメモリーが割り当てられます (関数は呼び出し可能なオブジェクトです)

// 関数式でもメモリーの割り当てが行われます
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

関数呼び出しを介して割り当て

一部の関数呼び出しでは、オブジェクトの割り当てが発生します。

var d = new Date(); // Date オブジェクトの割り当て

var e = document.createElement('div'); // DOM要素の割り当て

いくつかのメソッドは、新しい値またはオブジェクトを割り当てます:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 は新しい文字列
// JavaScript では文字列はイミュータブルな値なので、
// もしかすると実際にはメモリー割り当てが行われず、
// [0, 3]という文字の範囲だけを割り当てるかもしれません。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// a, a2 の内容を繋ぎ合わせた 4要素の配列が作成されました

値の使用

値を使用することは、基本的に割り当てられたメモリーで読み書きすることを意味します。これは変数やオブジェクトの値を読み書きすることや引数を関数に渡すことによって行われます。

メモリーが不要になったときに解放

メモリー管理の問題のほとんどは、この段階に来ます。ここで最も難しい作業は、「割り当てられたメモリーは、もはや必要とされていない」ときを発見することです。プログラム内のどこで、そのようなメモリーの断片が不要になって解放する必要があるかを決定するには、開発者による判断が必要なことが多いです。

高水準言語には、仕事がメモリー割当てを追跡することである「ガベージコレクタ」と呼ばれるソフトウェアを埋め込み、割り当てられたメモリーの一部がもはや、その場合に必要とされていないときに見つけるために使用し、それが自動的に解放されます。メモリーの一部のピースが必要とされているかどうかを知ることの一般的な問題は決定不能であるため、このプロセスは近似的なものです(アルゴリズムによって解決できません)。

ガベージコレクション

上述の通り、あるメモリーが"もはや必要ない"かどうかを自動的に知るという普遍的問題は決定不能です。そのため、ガベージコレクションのこの普遍的問題に対する解決策には制限があります。このセクションでは、メインのガベージコレクションのアルゴリズムとその限界を理解するために必要な概念を説明します。

リファレンス

ガベージコレクションアルゴリズムが依存している主な概念は、リファレンス (reference)の概念です。メモリー管理の文脈では、あるオブジェクトが別のオブジェクトに (明示的にであれ、暗黙的にであれ) アクセスできるとき、前者が後者を"参照している"と言います。例えば、JavaScript オブジェクトは自身の prototype (暗黙的な参照) とプロパティ値 (明示的な参照) への参照を持ちます。

ここでは、"オブジェクト"の概念は通常の JavaScript オブジェクトよりも広い概念として用いられており、また、関数のスコープ (もしくは、グローバルレキシカルスコープ) を含みます。

参照カウントのガベージコレクション

これは、最も素朴なガベージコレクションアルゴリズムです。このアルゴリズムは、"あるオブジェクトがもはや必要ない"ことを、"あるオブジェクトがその他のオブジェクトから参照されていない"ことと定義します。あるオブジェクトは、それに対する参照がゼロの時にガベージコレクト可能であると見なされます。

var o = { 
  a: {
    b:2
  }
}; 
// 2 個のオブジェクトが作成されました。一方はもう一方のプロパティとして参照されています。
// もう一方は変数 'o' に代入されているため、こちらも同じく参照されています。
// 明らかに、どちらのオブジェクトもガベージコレクションの対象になりません。


var o2 = o; // 'o2' がオブジェクトを参照するようになったため、
            // 参照カウントは 2 になりました
o = 1;      // 'o' はもうオブジェクトを参照していません。これで、このオブジェクトを
            // 参照するのは 'o2' だけになりました

var oa = o2.a; // プロパティ 'a' を参照しています。
               // 参照先のオブジェクトは、この時点で 2 つの箇所から参照されています。
               // ひとつはプロパティの値として、もうひとつは変数 'oa' の値としてです。

o2 = "yo"; // もともと 'o' に代入されていたオブジェクトを参照するものはいなくなりました。
           // このため、このオブジェクトはガベージコレクトの対象となります。
           // しかしながら、そのオブジェクトのプロパティ 'a' が指していたオブジェクトは
           // まだ変数 'oa' に参照されているため、解放することはできません。

oa = null; // もともと 'o' に代入されていたオブジェクトのプロパティ 'a' が指していた
           // オブジェクトへの参照が一つも無くなったため、ガベージコレクションの対象
           // となりました。

限界: 循環

循環に関しては限界があります。次の例では、2 つのオブジェクトが生成され、互いに参照しているため、循環を形成しています。これらは関数の呼び出し後にはスコープを外れるため、実際には役に立たず、本来であれば開放してもかまわないはずです。しかし、参照カウントアルゴリズムは、これら 2 つのオブジェクトがそれぞれ少なくとも 1 回参照されているため、どちらもガベージコレクトできないと見なします。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o references o2
  o2.a = o; // o2 references o

  return "azerty";
}

f();

Real-life example

Internet Explorer の 6 および 7 は、DOM オブジェクトに対しては参照カウント式のガベージコレクタを使用していることが知られています。循環は、メモリーリークを発生させることができるよくある間違いです:

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

上記の例では、DOM要素"myDivElement"は"circularReference"プロパティに自身への循環参照を持っています。そのプロパティが明示的に削除またはヌルにされていない場合, 参照カウントのガベージコレクタは、常に少なくとも 1 つの参照そのまま有することになり、それが DOM ツリーから削除された場合でも、メモリー内の DOM要素を維持します。DOM要素は、大量のデータを保持している場合("lotsOfData"プロパティで上記の例に示されています)、このデータによって消費されるメモリーが解放されることはありません。

マーク·アンド·スイープアルゴリズム

このアルゴリズムは、"あるオブジェクトがもはや必要ない"ことを、"あるオブジェクトが到達不能である"ことと定義します。

このアルゴリズムは、root と呼ばれるオブジェクトの集合についての知識を前提としています(JavaScript では、root はグローバルオブジェクトです)。定期的に、ガベージコレクタは、これらの root から開始し、これらの root から参照されるすべてのオブジェクト、それから、これらの中から参照されるすべてのオブジェクトなどを見つけます。root から開始すると、ガベージコレクタは、すべての到達可能オブジェクトを見つけ、すべての到達不能なオブジェクトをガベージコレクトします。

"あるオブジェクトが参照を持たない"ということは、そのオブジェクトは到達不能であるということなので、このアルゴリズムは前述のものよりも優れています。循環で見たように、逆は正しくありません。

2012年の時点で、すべての近代的なブラウザーでは、マークアンドスイープ式のガベージコレクタが含まれています。過去数年間で JavaScript のガベージコレクション(世代別/インクリメンタル/並行/並列ガベージコレクション)の分野で行われたすべての改善は、このアルゴリズムの実装の改善であって、ガベージコレクションアルゴリズム自体に対する改善でも、"もうオブジェクトが必要とされていない"と扱う基準を変えるものでもありません。

循環はもはや問題ではありません

最初の上記の例では、関数呼び出しが戻った後、2 つのオブジェクトは、グローバルオブジェクトから到達可能な何かによってもはや参照されません。その結果、それらは、ガベージコレクタによって到達不能として検出されます。

同じことは、第二の例でも言えます。div とそのハンドラが root から到達不能になったら、それらは両方ともお互いを参照するにもかかわらずガベージコレクトことができます。

制約: オブジェクトは明示的に到達不能にする必要がある

これを制約として取り上げましたが、実際には抵触することは滅多にありません。ガベージコレクションのことをたいてい誰もあまり気に留めないのはこのためです。

関連情報

ドキュメントのタグと貢献者

タグ: 
このページの貢献者: Uemmra3, ysgk, bokuweb, taketaku, yvt, shide55
最終更新者: Uemmra3,