ガベージコレクションがヒープをスイープするだけなのはなぜですか?


28

基本的に、これまでのところ、ガベージコレクションは現在指定されていないデータ構造を永久に消去することを学びました。ただし、これはそのような状態のヒープのみをチェックします。

データセクション(グローバル、定数など)やスタックもチェックしないのはなぜですか?ヒープについて、ガベージコレクションが必要な唯一のものは何ですか?


21
「ヒープをスイープ」は「スタックを強打」より安全です... :
ブライアン・ノブラウフ

回答:


62

ガベージコレクタースタックをスキャンします。現在、ヒープ上のものがスタック上のものによって使用されている(ポイントされている)ことを確認します。

スタックはそのように管理されていないため、ガベージコレクターがスタックメモリの収集を考慮することは意味がありません。スタック上のすべてが「使用中」であると見なされます。また、メソッド呼び出しから戻ると、スタックで使用されているメモリは自動的に解放されます。スタックスペースのメモリ管理は非常に単純で、安価で、簡単なので、ガベージコレクションを必要としないでしょう。

(スタックフレームがヒープに格納されるファーストクラスのオブジェクトであり、他のすべてのオブジェクトと同様にガベージコレクションされるsmalltalkなどのシステムがあります。しかし、これは最近の一般的なアプローチではありません。JavaのJVMとMicrosoftのCLRはハードウェアスタックと連続メモリを使用します)


7
+1スタックは常に完全に到達可能であるため、スイープする意味がありません
ラチェットフリーク

2
+1ありがとう、正しい答えを出すために4つの投稿を取りました。あなたは、スタック上のすべてが使用中であるために「とみなされる」と言っていた私はなぜそれが、知らない使用されているヒープがまだ使用中でオブジェクトとして、それ以上に強いAの意味として使用されて-それはの本当のnitpickです非常に良い答えです。
psr

@psr彼は、スタック上のすべてが強力に到達可能であり、メソッドが戻るまで収集する必要はないが、(RAII)はすでに明示的に管理されていることを意味します
ラチェットフリーク

@ratchetfreak-わかってる。そして、「考慮された」という言葉はおそらく必要ではないということを意味しました。それなしでより強力な声明を出すことは問題ありません。
psr

5
@psr:私は同意しません。「使用中とみなされる」は、非常に重要な理由により、スタックとヒープの両方に対してより正確です。必要なのは、再び使用されないものを破棄することです。あなたがすることはあなたが到達できないものを捨てることです。必要のない到達可能なデータがあるかもしれません。このデータが大きくなると、メモリリークが発生します(はい、多くの人が考えるとは異なり、GCの言語でも可能です)。また、スタックリークも発生すると主張するかもしれません。最も一般的な例は、末尾呼び出しを排除せずに実行される末尾再帰プログラムの不要なスタックフレームです(JVMなど)。
ブレイザーブレード

19

質問を好転させる。本当のやる気を起こさせる質問は、どのような状況でガベージコレクションのコストを回避できるかということです。

さて、最初のオフ、何しているガベージコレクションのコストは?主に2つのコストがあります。まず、何が生きているか判断する必要があります。潜在的に多くの作業が必要です。第二に、まだ生きている2つのものの間に割り当てられた何かを解放するときに形成される圧縮する必要があります。これらの穴は無駄です。しかし、それらを圧縮することも高価です。

これらのコストをどのように回避できますか?

明らかに、長命のものを割り当てないストレージ使用パターンを見つけ、短命のものを割り当て、その後長寿命のものを割り当てれば、ホールのコストを排除できます。ストレージの一部のサブセットについて、後続のすべての割り当てがそのストレージ内の以前の割り当てよりも短命であることを保証できる場合、そのストレージにホールはありません。

しかし、ホールの問題を解決した場合、ガベージコレクションの問題も解決しました。そのストレージにはまだ生きている何かがありますか?はい。寿命が長くなる前にすべてが割り当てられましたか?はい-その仮定は、穴の可能性を排除する方法です。したがって、あなたがする必要があるのは、「最新の割り当てが生きているか」ということだけです。そして、あなたはすべてがそのストレージで生きていることを知っています。

後続のすべての割り当てが前の割り当てよりも寿命が短いことがわかっているストレージ割り当てのセットがありますか?はい!メソッドのアクティベーションフレームは、それらを作成したアクティベーションよりも寿命が短いため、常に作成された順序とは逆の順序で破棄されます。

したがって、アクティブ化フレームをスタックに保存し、それらを収集する必要がないことを知ることができます。スタックにフレームがある場合、その下のフレームセット全体の寿命が長くなるため、それらを収集する必要はありません。そして、それらは作成されたのと反対の順序で破壊されます。したがって、アクティブ化フレームのガベージコレクションのコストはなくなります。

そもそもスタックに一時プールがある理由です。これは、メモリ管理のペナルティを負うことなくメソッドのアクティブ化を実装する簡単な方法だからです。

(もちろん、アクティベーションフレームの参照によって参照されるメモリのガベージコレクションのコストはまだ残っています。)

次に、アクティベーションフレームが予測可能な順序で破棄されない制御フローシステムについて考えます。短命のアクティベーションが長命のアクティベーションを引き起こす可能性がある場合はどうなりますか?ご想像のとおり、この世界では、スタックを使用してアクティベーションを収集する必要性を最適化することはできません。アクティベーションのセットには、再び穴を含めることができます。

C#2.0には、この機能がという形式でありyield returnます。yield returnを行うメソッドは、後で(MoveNextが次回呼び出されたときに)再アクティブ化されますが、いつそれが起こるかは予測できません。したがって、通常、イテレータブロックのアクティベーションフレームのスタックにある情報は代わりにヒープに格納され、列挙子が収集されるときにガベージコレクションされます。

同様に、C#およびVBの次のバージョンに搭載されている「async / await」機能を使用すると、メソッドのアクション中の明確なポイントでアクティベーションが「yield」および「resume」されるメソッドを作成できます。アクティベーションフレームは予測可能な方法で作成および破棄されなくなったため、スタックに格納されていたすべての情報をヒープに格納する必要があります。

数十年にわたって、厳密に順序付けられた方法で作成および破棄されるアクティベーションフレームを持つ言語が流行していると判断したのは、単なる歴史上の偶然です。現代の言語ではこのプロパティがますます不足しているため、スタックではなく、ガベージコレクションヒープへの継続を具体化する言語がますます増えると予想されます。


13

最も明白な答えは、おそらく完全ではありませんが、ヒープはインスタンスデータの場所です。インスタンスデータとは、実行時に作成されるクラス(別名オブジェクト)のインスタンスを表すデータを意味します。このデータは本質的に動的であり、これらのオブジェクトの数、したがってそれらが占有するメモリの量は、実行時にのみ知られています。このメモリの回復に苦痛があったり、長時間実行されているプログラムが時間とともにメモリをすべて消費したりします。

クラス定義、定数、およびその他の静的データ構造によって消費されるメモリは、本質的に未チェックで増加する可能性は低いです。そのクラスの実行時インスタンスの不明な数ごとにメモリにはクラス定義が1つしかないため、このタイプの構造はメモリ使用量に対する脅威ではないことは理にかなっています。


5
ただし、ヒープは「インスタンスデータ」の場所ではありません。それらもスタックに配置できます。
svick

@svickもちろん言語に依存します。Javaはヒープに割り当てられたオブジェクトのみをサポートし、Valaはヒープに割り当てられた(クラス)とスタックに割り当てられた(構造)を明確に区別します。
ふわふわ

1
@fluffy:これらは非常に限られた言語であり、言語が厳密化されていないため、これが一般的に成り立つとは考えられません。
マチューM.

なつみ それは私のポイントのようなものでした。
ふわふわ

@fluffy:なぜクラスはヒープに割り当てられ、構造体はスタックに割り当てられるのですか?
ダークテンプラー

10

ガベージコレクションがある理由に留意する価値があります。メモリの割り当てを解除するタイミングを知ることが難しい場合があるためです。あなたは本当にヒープにこの問題があります。スタックに割り当てられたデータは、最終的に割り当て解除されるため、ガベージコレクションを行う必要はありません。通常、データセクションの内容は、プログラムの有効期間中に割り当てられると想定されています。


1
「最終的に」割り当てが解除されるだけでなく、適切なタイミングで割り当てが解除されます。
ボリスヤンコフ

3
  1. これらのサイズは予測可能で(スタックを除き一定であり、スタックは通常数MBに制限されます)、通常は非常に小さいです(少なくとも数百MBの大きなアプリケーションが割り当てる場合と比較して)。

  2. 通常、動的に割り当てられたオブジェクトには、到達可能な短い時間枠があります。その後、それらを再び参照する方法はありません。これとは対照的に、データセクションのエントリ、グローバル変数など:頻繁に、それらを直接参照するコードがあります(考えてみてくださいconst char *foo() { return "foo"; })。通常、コードは変更されないので、参照は残り、関数が呼び出されるたびに別の参照が作成されます(コンピューターが知っている限りいつでも可能です-停止問題を解決しない限り、 )。したがって、常に到達可能なため、そのメモリのほとんどを解放することはできませんでした

  3. 多くのガベージコレクション言語では、実行中のプログラムに属するすべてのものがヒープに割り当てられます。Pythonでは、単にデータセクションがなく、スタックに割り当てられた値はありません(ローカル変数の参照があり、呼び出しスタックがありますが、どちらもintC と同じ意味の値ではありません)。すべてのオブジェクトはヒープ上にあります。


「Pythonには、データセクションはありません」。これは厳密には真実ではありません。私が理解しているように、データセクションにはNone、True、Falseが割り当てられています。stackoverflow.com
Jason Baker

@JasonBaker:興味深い発見!ただし、効果はありません。これは実装の詳細であり、組み込みオブジェクトに制限されています。それは、それらのオブジェクトの割り当てが解除されると予想されていないことを言及することはありません、これまでとにかくプログラムの一生の間に、ではない、ともサイズが小さなされている(各バイト未満32、私は推測すると思います)。

@delnan Eric Lippertが指摘したように、ほとんどの言語では、スタックとヒープに別々のメモリ領域が存在することが実装の詳細です。あなたは(あなたが行うときのパフォーマンスが苦しむかもしれないが)すべてのスタックを使用せずに、ほとんどの言語を実装し、まだ彼らの仕様に準拠することができます
ジュール・

2

他の多くのレスポンダーが言ったように、スタックはルートセットの一部であるため、参照のためにスキャンされますが、それ自体は「収集」されません。

スタック上のゴミが問題にならないことを示唆するコメントのいくつかに返信したいだけです。ヒープ上のより多くのゴ​​ミが到達可能と見なされる可能性があるためです。良心的なVMおよびコンパイラのライターは、スタックの無効部分をスキャンから除外するか、除外します。IIRC、一部のVMにはPCの範囲をスタックスロットの活性ビットマップにマッピングするテーブルがあり、他のVMにはスロットを無効にするものがあります。現在どのテクニックが好まれているのかわかりません。

この特定の考慮事項を説明するために使用される1つの用語は、安全な空間です。


知るのは面白いでしょう。最初に考えたのは、スペースを空にすることが最も現実的だということです。除外されたエリアのツリーをたどるのは、単にヌルをスキャンするよりも時間がかかる場合があります。明らかに、スタックを圧縮する試みには危険が伴います!その作業を行うことは、心を曲げる/エラーを起こしやすいプロセスのように聞こえます。
ブライアン

@ブライアン、実際には、それについてもう少し考えて、型付きVMにはそのようなものが必要なので、どのスロットが整数や浮動小数点数ではなく参照であるかを判断できます。また、スタックの圧縮については、「CONS Shouldヘンリー・ベイカーによる議論を否定しない。
ライアンカルペッパー

スロットの種類を決定し、それらが適切に使用されていることを確認することは、コンパイル時(信頼できるバイトコードを使用するVMの場合)またはロード時(バイトコードが信頼できないソース(Javaなど)から取得される場合)に静的に実行できます。
ジュール

1

あなたと他の多くの人が間違ったいくつかの基本的な誤解を指摘させてください。

「ガベージコレクションがヒープをスイープするのはなぜですか?」それは逆です。最も単純で、最も保守的で、最も遅いガベージコレクターだけがヒープをスイープします。それが彼らがとても遅い理由です。

高速ガベージコレクターは、スタック(およびオプションで、FFIポインターのグローバル、ライブポインターのレジスタなど、他のルート)をスイープし、スタックオブジェクトが到達可能なポインターのみをコピーします。残りは破棄されます(つまり無視されます)。ヒープでのスキャンはまったく行われません。

ヒープはスタックよりも約1000倍大きいため、このようなスタックスキャンGCは通常、はるかに高速です。通常サイズのヒープでは250msに対して15ms以内。あるスペースから別のスペースにオブジェクトをコピー(移動)するため、ほとんどがセミスペースコピーコレクターと呼ばれ、2倍のメモリが必要であり、メモリの少ない携帯電話のような非常に小さなデバイスではほとんど使用できません。コンパクトであるため、単純なマーク&スイープヒープスキャナーとは異なり、後でキャッシュフレンドリーになります。

ポインターを移動するため、FFI、ID、および参照は注意が必要です。通常、IDはランダムなID、転送ポインターを介した参照で解決されます。FFIは、外部オブジェクトが古いスペースへのポインターを保持できないため、注意が必要です。FFIポインターは、通常、別個のヒープアリーナに保持されます。たとえば、遅いmark&sweep、静的コレクターなどです。または、refcountingを使用した簡単なmalloc。mallocには大きなオーバーヘッドがあり、さらに再カウントすることに注意してください。

マーク&スイープは実装するのは簡単ですが、実際のプログラムでは使用しないでください。特に、標準のコレクターとして教えてはいけません。このような高速のスタックスキャンコピーコレクターで最も有名なのは、チェイニーの2本指コレクターです。


問題は、特定のガベージコレクションアルゴリズムではなく、メモリのどの部分がガベージコレクションされるかについてのようです。最後の文は、OPがガベージコレクションを実装する特定のメカニズムではなく、「ガベージコレクション」の一般的な同義語として「スイープ」を使用していることを特に示しています。それを考慮すると、あなたの答えは、最も単純なガベージコレクターのみがヒープをガベージコレクションし、代わりに高速ガベージコレクターがスタックと静的メモリをガベージコレクションし、ヒープがメモリを使い果たすまで成長し続けるということです。
8bittree

いいえ、質問は非常に具体的で巧妙でした。答えはそうではありません。低速マーク&スイープGCには、スタックのルートをスキャンするマークステップと、ヒープをスキャンするスイープフェーズの2つのフェーズがあります。高速コピーGCには、スタックをスキャンする1つのフェーズしかありません。そのように簡単。ここでは適切なガベージコレクターについて誰も知らないので、質問に答える必要があります。あなたの解釈は乱暴です。
-rurban

0

スタックに何が割り当てられますか?ローカル変数と戻りアドレス(Cで)。関数が戻ると、そのローカル変数は破棄されます。スタックをスイープすることは必要ではなく、有害ですらありません。

多くの動的言語、およびJavaまたはC#は、多くの場合Cのシステムプログラミング言語で実装されます。JavaはC関数で実装され、Cローカル変数を使用するため、Javaのガベージコレクターはスタックをスイープする必要はありません。

興味深い例外があります:Chicken Schemeのガベージコレクター、スタックをガベージコレクションの第1世代のスペースとして使用するため、スタックを一掃します(Chicken Scheme Design Wikipediaを参照)。

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