回答:
ガベージコレクターはスタックをスキャンします。現在、ヒープ上のものがスタック上のものによって使用されている(ポイントされている)ことを確認します。
スタックはそのように管理されていないため、ガベージコレクターがスタックメモリの収集を考慮することは意味がありません。スタック上のすべてが「使用中」であると見なされます。また、メソッド呼び出しから戻ると、スタックで使用されているメモリは自動的に解放されます。スタックスペースのメモリ管理は非常に単純で、安価で、簡単なので、ガベージコレクションを必要としないでしょう。
(スタックフレームがヒープに格納されるファーストクラスのオブジェクトであり、他のすべてのオブジェクトと同様にガベージコレクションされるsmalltalkなどのシステムがあります。しかし、これは最近の一般的なアプローチではありません。JavaのJVMとMicrosoftのCLRはハードウェアスタックと連続メモリを使用します)
質問を好転させる。本当のやる気を起こさせる質問は、どのような状況でガベージコレクションのコストを回避できるかということです。
さて、最初のオフ、何しているガベージコレクションのコストは?主に2つのコストがあります。まず、何が生きているかを判断する必要があります。潜在的に多くの作業が必要です。第二に、まだ生きている2つのものの間に割り当てられた何かを解放するときに形成される穴を圧縮する必要があります。これらの穴は無駄です。しかし、それらを圧縮することも高価です。
これらのコストをどのように回避できますか?
明らかに、長命のものを割り当てないストレージ使用パターンを見つけ、短命のものを割り当て、その後長寿命のものを割り当てれば、ホールのコストを排除できます。ストレージの一部のサブセットについて、後続のすべての割り当てがそのストレージ内の以前の割り当てよりも短命であることを保証できる場合、そのストレージにホールはありません。
しかし、ホールの問題を解決した場合、ガベージコレクションの問題も解決しました。そのストレージにはまだ生きている何かがありますか?はい。寿命が長くなる前にすべてが割り当てられましたか?はい-その仮定は、穴の可能性を排除する方法です。したがって、あなたがする必要があるのは、「最新の割り当てが生きているか」ということだけです。そして、あなたはすべてがそのストレージで生きていることを知っています。
後続のすべての割り当てが前の割り当てよりも寿命が短いことがわかっているストレージ割り当てのセットがありますか?はい!メソッドのアクティベーションフレームは、それらを作成したアクティベーションよりも寿命が短いため、常に作成された順序とは逆の順序で破棄されます。
したがって、アクティブ化フレームをスタックに保存し、それらを収集する必要がないことを知ることができます。スタックにフレームがある場合、その下のフレームセット全体の寿命が長くなるため、それらを収集する必要はありません。そして、それらは作成されたのと反対の順序で破壊されます。したがって、アクティブ化フレームのガベージコレクションのコストはなくなります。
そもそもスタックに一時プールがある理由です。これは、メモリ管理のペナルティを負うことなくメソッドのアクティブ化を実装する簡単な方法だからです。
(もちろん、アクティベーションフレームの参照によって参照されるメモリのガベージコレクションのコストはまだ残っています。)
次に、アクティベーションフレームが予測可能な順序で破棄されない制御フローシステムについて考えます。短命のアクティベーションが長命のアクティベーションを引き起こす可能性がある場合はどうなりますか?ご想像のとおり、この世界では、スタックを使用してアクティベーションを収集する必要性を最適化することはできません。アクティベーションのセットには、再び穴を含めることができます。
C#2.0には、この機能がという形式でありyield return
ます。yield returnを行うメソッドは、後で(MoveNextが次回呼び出されたときに)再アクティブ化されますが、いつそれが起こるかは予測できません。したがって、通常、イテレータブロックのアクティベーションフレームのスタックにある情報は代わりにヒープに格納され、列挙子が収集されるときにガベージコレクションされます。
同様に、C#およびVBの次のバージョンに搭載されている「async / await」機能を使用すると、メソッドのアクション中の明確なポイントでアクティベーションが「yield」および「resume」されるメソッドを作成できます。アクティベーションフレームは予測可能な方法で作成および破棄されなくなったため、スタックに格納されていたすべての情報をヒープに格納する必要があります。
数十年にわたって、厳密に順序付けられた方法で作成および破棄されるアクティベーションフレームを持つ言語が流行していると判断したのは、単なる歴史上の偶然です。現代の言語ではこのプロパティがますます不足しているため、スタックではなく、ガベージコレクションヒープへの継続を具体化する言語がますます増えると予想されます。
最も明白な答えは、おそらく完全ではありませんが、ヒープはインスタンスデータの場所です。インスタンスデータとは、実行時に作成されるクラス(別名オブジェクト)のインスタンスを表すデータを意味します。このデータは本質的に動的であり、これらのオブジェクトの数、したがってそれらが占有するメモリの量は、実行時にのみ知られています。このメモリの回復に苦痛があったり、長時間実行されているプログラムが時間とともにメモリをすべて消費したりします。
クラス定義、定数、およびその他の静的データ構造によって消費されるメモリは、本質的に未チェックで増加する可能性は低いです。そのクラスの実行時インスタンスの不明な数ごとにメモリにはクラス定義が1つしかないため、このタイプの構造はメモリ使用量に対する脅威ではないことは理にかなっています。
ガベージコレクションがある理由に留意する価値があります。メモリの割り当てを解除するタイミングを知ることが難しい場合があるためです。あなたは本当にヒープにこの問題があります。スタックに割り当てられたデータは、最終的に割り当て解除されるため、ガベージコレクションを行う必要はありません。通常、データセクションの内容は、プログラムの有効期間中に割り当てられると想定されています。
これらのサイズは予測可能で(スタックを除き一定であり、スタックは通常数MBに制限されます)、通常は非常に小さいです(少なくとも数百MBの大きなアプリケーションが割り当てる場合と比較して)。
通常、動的に割り当てられたオブジェクトには、到達可能な短い時間枠があります。その後、それらを再び参照する方法はありません。これとは対照的に、データセクションのエントリ、グローバル変数など:頻繁に、それらを直接参照するコードがあります(考えてみてくださいconst char *foo() { return "foo"; }
)。通常、コードは変更されないので、参照は残り、関数が呼び出されるたびに別の参照が作成されます(コンピューターが知っている限りいつでも可能です-停止問題を解決しない限り、 )。したがって、常に到達可能なため、そのメモリのほとんどを解放することはできませんでした。
多くのガベージコレクション言語では、実行中のプログラムに属するすべてのものがヒープに割り当てられます。Pythonでは、単にデータセクションがなく、スタックに割り当てられた値はありません(ローカル変数の参照があり、呼び出しスタックがありますが、どちらもint
C と同じ意味の値ではありません)。すべてのオブジェクトはヒープ上にあります。
他の多くのレスポンダーが言ったように、スタックはルートセットの一部であるため、参照のためにスキャンされますが、それ自体は「収集」されません。
スタック上のゴミが問題にならないことを示唆するコメントのいくつかに返信したいだけです。ヒープ上のより多くのゴミが到達可能と見なされる可能性があるためです。良心的なVMおよびコンパイラのライターは、スタックの無効部分をスキャンから除外するか、除外します。IIRC、一部のVMにはPCの範囲をスタックスロットの活性ビットマップにマッピングするテーブルがあり、他のVMにはスロットを無効にするものがあります。現在どのテクニックが好まれているのかわかりません。
この特定の考慮事項を説明するために使用される1つの用語は、安全な空間です。
あなたと他の多くの人が間違ったいくつかの基本的な誤解を指摘させてください。
「ガベージコレクションがヒープをスイープするのはなぜですか?」それは逆です。最も単純で、最も保守的で、最も遅いガベージコレクターだけがヒープをスイープします。それが彼らがとても遅い理由です。
高速ガベージコレクターは、スタック(およびオプションで、FFIポインターのグローバル、ライブポインターのレジスタなど、他のルート)をスイープし、スタックオブジェクトが到達可能なポインターのみをコピーします。残りは破棄されます(つまり無視されます)。ヒープでのスキャンはまったく行われません。
ヒープはスタックよりも約1000倍大きいため、このようなスタックスキャンGCは通常、はるかに高速です。通常サイズのヒープでは250msに対して15ms以内。あるスペースから別のスペースにオブジェクトをコピー(移動)するため、ほとんどがセミスペースコピーコレクターと呼ばれ、2倍のメモリが必要であり、メモリの少ない携帯電話のような非常に小さなデバイスではほとんど使用できません。コンパクトであるため、単純なマーク&スイープヒープスキャナーとは異なり、後でキャッシュフレンドリーになります。
ポインターを移動するため、FFI、ID、および参照は注意が必要です。通常、IDはランダムなID、転送ポインターを介した参照で解決されます。FFIは、外部オブジェクトが古いスペースへのポインターを保持できないため、注意が必要です。FFIポインターは、通常、別個のヒープアリーナに保持されます。たとえば、遅いmark&sweep、静的コレクターなどです。または、refcountingを使用した簡単なmalloc。mallocには大きなオーバーヘッドがあり、さらに再カウントすることに注意してください。
マーク&スイープは実装するのは簡単ですが、実際のプログラムでは使用しないでください。特に、標準のコレクターとして教えてはいけません。このような高速のスタックスキャンコピーコレクターで最も有名なのは、チェイニーの2本指コレクターです。
スタックに何が割り当てられますか?ローカル変数と戻りアドレス(Cで)。関数が戻ると、そのローカル変数は破棄されます。スタックをスイープすることは必要ではなく、有害ですらありません。
多くの動的言語、およびJavaまたはC#は、多くの場合Cのシステムプログラミング言語で実装されます。JavaはC関数で実装され、Cローカル変数を使用するため、Javaのガベージコレクターはスタックをスイープする必要はありません。
興味深い例外があります:Chicken Schemeのガベージコレクターは、スタックをガベージコレクションの第1世代のスペースとして使用するため、スタックを一掃します(Chicken Scheme Design Wikipediaを参照)。