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