JavaScriptクロージャがガベージコレクションされる方法


168

私は次のChromeバグをログに記録しました。これにより、コードに重大で明白でない多くのメモリリークが発生しました。

(これらの結果は、GCを実行するChrome Dev Toolsのメモリプロファイラを使用してから、収集されたガベージングされていないものすべてのヒープスナップショットを取得します。)

以下のコードでは、someClassインスタンスはガベージコレクションされています(良好)。

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

ただし、この場合はガベージコレクションされません(不良)。

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

そして対応するスクリーンショット:

Chromebugのスクリーンショット

function() {}オブジェクトが同じコンテキストで他のクロージャによって参照されている場合、クロージャ自体が到達可能かどうかにかかわらず、クロージャ(この場合は)はすべてのオブジェクトを「有効」に保つようです。

私の質問は、他のブラウザー(IE 9+およびFirefox)でのクロージャーのガベージコレクションについてです。私はJavaScriptヒーププロファイラーなどのWebkitのツールに精通していますが、他のブラウザーのツールについてはほとんど知らないため、これをテストすることができませんでした。

これら3つのケースのどれでIE9 +とFirefoxのガベージが インスタンスを収集しsomeClass ますか?


4
初心者のために、Chromeではどのようにガベージコレクションが行われる変数/オブジェクトをテストできますか?
nnnnnn 2013年

1
多分コンソールはそれへの参照を保持しています。コンソールをクリアすると、GCされますか?
デビッド2013年

1
@david最後の例では、unreachable関数は実行されないため、実際には何も記録されません。
James Montagne 2013年

1
事実に直面しているように見えても、その重要なバグが発生したとは信じられません。しかし、私は何度もコードを見ており、他の合理的な説明は見つかりません。コンソールでコードを実行しないようにしようとしましたか(ロードされたスクリプトからブラウザに自然にコードを実行させます)?
plalx 2013年

1
@some、私は以前その記事を読んだことがあります。これは「JavaScriptアプリケーションでの循環参照の処理」というタイトルですが、JS / DOM循環参照の問題は、最新のブラウザーには当てはまりません。クロージャーについて言及していますが、すべての例で、問題の変数はプログラムによってまだ使用されていました。
Paul Draper

回答:


78

私の知る限り、これはバグではなく、予想される動作です。

Mozillaのメモリ管理ページから:「2012年の時点で、すべての最新ブラウザはマークアンドスイープガベージコレクタを出荷しています。」「制限:オブジェクトは明示的に到達不能にする必要があります

それが失敗するあなたの例でsomeは、クロージャでまだ到達可能です。私はそれを到達不能にするために2つの方法を試してみましたが、どちらも機能しました。some=null不要になったときに設定するか、設定window.f_ = null;すると消えます。

更新

WindowsのChrome 30、FF25、Opera 12、IE10で試してみました。

標準では、ガベージコレクションについては何も言うが、どうするかのいくつかの手がかりを与えていません。

  • セクション13関数の定義、ステップ4:「13.2で指定された新しい関数オブジェクトを作成した結果、クロージャになる」
  • セクション13.2「スコープによって指定されたレキシカル環境」(スコープ=クロージャ)
  • セクション10.2字句環境:

「(内部の)字句環境の外部参照とは、内部の字句環境を論理的に囲む字句環境への参照です。

もちろん、外部の語彙環境は、独自の外部の語彙環境を持つことができます。字句環境は、複数の内側の字句環境の外部環境として機能します。たとえば、関数宣言は 2つの入れ子含ま関数宣言を次に入れ子関数の各々の字句環境は、それらの外側の字句環境として、周囲の機能の現在の実行の字句環境を有するであろう。」

したがって、関数は親の環境にアクセスできます。

したがって、some戻り関数のクロージャーで使用できる必要があります。

では、なぜそれが常に利用可能ではないのですか?

ChromeとFFは、場合によっては変数を削除するのに十分スマートなようですが、OperaとIEの両方でsomeクロージャーで変数を使用できます(注:これを表示してブレークポイントを設定しreturn null、デバッガーをチェックします)。

GCはsome、関数で使用されているかどうかを検出するように改善できますが、複雑になります。

悪い例:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

上記の例では、GCは変数が使用されているかどうかを知る方法がありません(コードがテストされ、Chrome30、FF25、Opera 12、IE10で機能します)。

への別の値の割り当てによってオブジェクトへの参照が壊れた場合、メモリは解放されますwindow.f_

私の意見では、これはバグではありません。


4
ただし、コールバックが実行されるとsetTimeout()setTimeout()コールバックの関数スコープが実行され、そのスコープ全体がガベージコレクションされ、への参照が解放されsomeます。someクロージャー内ののインスタンスに到達できる実行可能なコードはもうありません。ガベージコレクションする必要があります。最後の例は、unreachable()呼び出されておらず、誰も参照していないため、さらに悪い例です。そのスコープもGCする必要があります。これらは両方ともバグのようです。JSには、関数スコープで「解放」するための言語要件はありません。
jfriend00 2013年

1
@someすべきではない。関数は、内部で使用していない変数を閉じることは想定されていません。
plalx 2013年

2
空の関数からアクセスできますが、実際には参照されていないため、明確になっているはずです。ガベージコレクションは、実際の参照を追跡します。参照されている可能性のあるすべてのものを保持することは想定されておらず、実際に参照されているものだけが保持されます。ラストf()が呼び出されると、someそれ以上の実際の参照はありません。到達不可能であり、GCする必要があります。
jfriend00 2013年

1
@ jfriend00(標準)[ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf]で何も見つからないので、内部で使用する変数のみについて何でも利用できるようになっています。セクション13では、プロダクションステップ4:13.2、10.2指定されているように、新しいFunctionオブジェクトを作成した結果としてクロージャを作成します。「外部環境参照は、字句環境値の論理的なネストをモデル化するために使用されます。 )字句環境は、字句環境を論理的に囲む字句環境への参照です。」
いくつかの

2
まあ、eval本当に特別なケースです。たとえば、evalエイリアスを付けることはできません(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…var eval2 = evalevalが使用されている場合(別の名前で呼び出すことはできないため、簡単に実行できます)、スコープ内の任意のものを使用できると想定する必要があります。
Paul Draper

49

IE9 +とFirefoxでテストしました。

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

こちらのライブサイト。

function() {}最小限のメモリを使用して、500 の配列で仕上げたいと思いました。

残念ながら、そうではありませんでした。空の各関数は、100万個の数値の(永遠に到達できないが、GCされない)配列を保持します。

Chromeは最終的に停止して終了し、Firefoxは4GB近くのRAMを使用した後、すべてを終了し、IEは「メモリ不足」を示すまで漸進的に遅くなります。

コメント行のいずれかを削除すると、すべてが修正されます。

これら3つのブラウザー(Chrome、Firefox、IE)はすべて、クロージャーごとではなく、コンテキストごとに環境レコードを保持しているようです。ボリスは、この決定の背後にある理由はパフォーマンスであると仮定しています。それは可能性が高いと思われますが、上記の実験に照らして、パフォーマンスがどの程度呼び出されるかはわかりません。

クロージャー参照が必要な場合some(ここではそれを使用しなかったと仮定しますが、使用すると想像してください)、代わりに

function g() { some; }

私が使う

var g = (function(some) { return function() { some; }; )(some);

クロージャーを他の関数とは異なるコンテキストに移動することで、メモリの問題を修正します。

これは私の人生をはるかに退屈なものにします。

PS好奇心から、私はこれをJavaで試しました(関数内でクラスを定義する機能を使用)。GCは、当初JavaScriptに期待していたとおりに機能します。


外部関数var g =(function(some){return function(){some;};})(some);の閉じ括弧が抜けていると思います
HCJ 2014年

15

ヒューリスティックスはさまざまですが、この種のものを実装する一般的な方法f()は、ケースの各呼び出しに対して環境レコードを作成し、そのローカルのローカルのみfを(何らかのクロージャーによって)閉じられている環境レコードに格納することです。次にf、環境レコードを維持するための呼び出しで作成されたクロージャ。これが、少なくともFirefoxがクロージャを実装する方法だと思います。

これには、クローズされた変数への高速アクセスと実装の単純さという利点があります。これには観測された効果の欠点があります。ある変数に対する短期間のクロージャのクローズは、それが長期のクロージャによって維持される原因になります。

実際にクローズするものに応じて、さまざまなクロージャーに対して複数の環境レコードを作成してみることができますが、非常に複雑になり、パフォーマンスとメモリ自体の問題を引き起こす可能性があります...


あなたの洞察をありがとう。これもChromeがクロージャを実装する方法でもあると結論づけました。私はいつもそれらが後者の方法で実装されると常に思っていました。つまり、各クロージャーは必要な環境のみを保持していましたが、そうではありませんでした。複数の環境レコードを作成するのは本当にそれほど複雑なことなのでしょうか。クロージャーの参照を集約するのではなく、それぞれが唯一のクロージャーであるかのように振る舞います。私は、パフォーマンスの考慮がここでの推論であると思っていましたが、共有環境のレコードを持つことの結果はさらに悪いように見えます。
Paul Draper

後者の方法では、作成する必要のある環境レコードの数が急増する場合があります。可能な場合は関数間でそれらを共有しようと懸命に努力しない限り、そのためには複雑な機械がたくさん必要です。それは可能ですが、パフォーマンスのトレードオフは現在のアプローチを支持すると聞いています。
Boris Zbarsky 2013年

レコードの数は、作成されたクロージャの数と同じです。私が説明したかもしれませんO(n^2)またはO(2^n)爆発ではなく、比例的な増加など。
Paul Draper

まあ、O(N)はO(1)に比べて爆発的です。特に、それぞれがかなりの量のメモリを占有する可能性がある場合...繰り返しますが、私はこれについての専門家ではありません。irc.mozilla.orgの#jsapiチャネルで質問すると、トレードオフが何であるかを説明するよりも、より詳細で詳細な説明が得られる可能性があります。
Boris Zbarsky 2013年

1
@Esailija残念ながら、実際にはかなり一般的です。必要なのは、関数内の大きな一時変数(通常は大きな型付き配列)であり、ランダムな短期のコールバックが使用し、長期のクロージャーを使用します。最近、ウェブアプリを書いている人のために何度も登場しています...
Boris Zbarsky 2013年

0
  1. 関数呼び出し間で状態を維持する関数add()があり、それに渡されたすべての値をいくつかの呼び出しで追加して、合計を返したいとします。

add(5);のように // 5を返します

add(20); // 25(5 + 20)を返します

add(3); // 28(25 + 3)を返します

これを行う最初の2つの方法は、通常、グローバル変数を定義する ことです。もちろん、合計を保持するためにグローバル変数を使用できます。ただし、グローバルを使用する場合、この男はあなたを生食することを覚えておいてください。

グローバル変数を定義せずにクロージャ使用する最新の方法

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());


0

function Country(){
    console.log("makesure country call");	
   return function State(){
   
    var totalstate = 0;	
	
	if(totalstate==0){	
	
	console.log("makesure statecall");	
	return function(val){
      totalstate += val;	 
      console.log("hello:"+totalstate);
	   return totalstate;
    }	
	}else{
	 console.log("hey:"+totalstate);
	}
	 
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d


答えを説明してください
janith1024

0

(function(){

   function addFn(){

    var total = 0;
	
	if(total==0){	
	return function(val){
      total += val;	 
      console.log("hello:"+total);
	   return total+9;
    }	
	}else{
	 console.log("hey:"+total);
	}
	 
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
	console.log("r:"+r); //14 
	var r= add(20);  //25
	console.log("r:"+r); //34
	var r= add(10);  //35
	console.log("r:"+r);  //44
	
	
var addB = addFn();
	 var r= addB(6);  //6
	 var r= addB(4);  //10
	  var r= addB(19);  //29
    
  
}());

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.