.NETのガベージコレクションについて


170

以下のコードを考えてみましょう:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

さて、メインメソッドの変数c1はスコープ外にありGC.Collect()、呼び出されたときに他のオブジェクトによってさらに参照されていなくても、なぜそこでファイナライズされないのですか?


8
インスタンスがスコープ外の場合、GCはすぐにインスタンスを解放しません。必要だと思ったときにそうします。GCに関するすべてをここで読むことができます:msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061

@ user1908061(Pssst。リンクが壊れています。)
Dragomok '16

回答:


352

デバッガーを使用しているため、ここでつまずき、非常に間違った結論が出ています。ユーザーのマシンで実行する方法でコードを実行する必要があります。最初にビルド+構成マネージャーを使用してリリースビルドに切り替え、左上隅の[アクティブソリューション構成]コンボを[リリース]に変更します。次に、[ツール+オプション]、[デバッグ]、[全般]の順に進み、[JIT最適化を抑制する]オプションのチェックを外します。

次に、プログラムを再度実行し、ソースコードをいじります。余分なブレースがまったく効果がないことに注意してください。また、変数をnullに設定しても違いはありません。常に「1」を出力します。これで、期待どおりに機能し、期待どおりに機能します。

デバッグビルドを実行したときに動作が異なる理由を説明するタスクは残ります。そのためには、ガベージコレクターがローカル変数を検出する方法と、デバッガーが存在することによる影響を説明する必要があります。

まず、ジッタは2つ実行されます、メソッドのILをマシンコードにコンパイルするときに重要な役割を果たします。最初のものはデバッガーで非常によく見えます、あなたはDebug + Windows + Disassemblyウィンドウでマシンコードを見ることができます。しかし、第二の義務は完全に見えません。また、メソッド本体内のローカル変数の使用方法を説明するテーブルも生成します。そのテーブルには、各メソッド引数と2つのアドレスを持つローカル変数のエントリがあります。変数がオブジェクト参照を最初に格納するアドレス。そして、その変数が使用されなくなったマシンコード命令のアドレス。また、その変数がスタックフレームまたはCPUレジスタに格納されているかどうか。

このテーブルはガベージコレクタに不可欠です。コレクションを実行するときに、オブジェクト参照を探す場所を知る必要があります。参照がGCヒープ上のオブジェクトの一部である場合は、とても簡単です。オブジェクト参照がCPUレジスタに格納されている場合は、明らかに簡単ではありません。テーブルはどこを見ればいいかを示しています。

テーブル内の「使用されなくなった」アドレスは非常に重要です。これにより、ガベージコレクタが非常に効率的になります。メソッド内で使用され、そのメソッドがまだ実行を完了していない場合でも、オブジェクト参照を収集できます。これは非常に一般的です。たとえば、Main()メソッドは、プログラムが終了する直前にのみ実行を停止します。明らかに、Main()メソッド内で使用されるオブジェクト参照がプログラムの実行中に存続することを望まない場合、リークにつながります。ジッターはテーブルを使用して、プログラムが呼び出しを行う前にMain()メソッド内でプログラムがどれだけ進んだかによって、そのようなローカル変数がもはや役に立たないことを発見できます。

そのテーブルに関連するほとんど魔法のメソッドは、GC.KeepAlive()です。これは非常に特殊な方法であり、コードをまったく生成しません。その唯一の義務は、そのテーブルを変更することです。これは、拡張しますローカル変数の存続期間。格納されている参照がガベージコレクションを実行するのを防ぎます。これを使用する必要があるのは、GCが参照の収集に熱心すぎないようにする場合のみです。これは、参照がアンマネージコードに渡される相互運用シナリオで発生する可能性があります。ガベージコレクターは、ジッターによってコンパイルされなかったため、参照を探す場所を示すテーブルがないため、そのようなコードによって使用されている参照を確認できません。EnumWindows()のようなアンマネージ関数にデリゲートオブジェクトを渡すのは、GC.KeepAlive()を使用する必要がある場合のボイラープレートの例です。

したがって、Releaseビルドで実行した後のサンプルスニペットからわかるように、ローカル変数、メソッドの実行が完了する前の早い段階で収集される可能性があります。さらに強力なことに、そのメソッドがこれを参照しなくなった場合、そのメソッドの実行中にオブジェクトを収集できます。それには問題があり、そのようなメソッドをデバッグするのは非常に厄介です。変数を[ウォッチ]ウィンドウに配置するか、検査することができます。そして、GCが発生した場合、デバッグ中に消えます。それは非常に不愉快なことなので、ジッターはデバッガーが接続されていることを認識しています。次に変更しますテーブルと「最後に使用された」アドレスを変更します。そして、それを通常の値からメソッドの最後の命令のアドレスに変更します。これにより、メソッドが返されない限り、変数が維持されます。これにより、メソッドが戻るまで監視を続けることができます。

これは、以前に見たものと、なぜ質問したのかについても説明します。GC.Collect呼び出しは参照を収集できないため、「0」を出力します。この表は、変数がメソッドの最後まで、GC.Collect()呼び出しを過ぎて使用されていることを示しています。デバッガーを接続、Debugビルドを実行することにより、そのように強制されます。

変数をnullに設定しても効果があります。GCが変数を検査し、参照を参照しなくなるためです。しかし、多くのC#プログラマーが陥る罠に陥らないようにしてください。実際にそのコードを書くのは無意味です。リリースビルドでコードを実行するときに、そのステートメントが存在するかどうかに関係なく、違いはありません。実際、ジッターオプティマイザーはその影響をまったく受けないため、そのステートメントを削除します。そのため、効果があるように見えても、そのようなコードは記述しないでください。


このトピックに関する最後の注意点の1つは、Officeアプリで何かをする小さなプログラムを作成するプログラマーを困らせることです。デバッガーは通常、それらを間違ったパスで取得し、Officeプログラムをオンデマンドで終了させます。これを行う適切な方法は、GC.Collect()を呼び出すことです。しかし、彼らはアプリをデバッグするときにそれが機能しないことを発見し、Marshal.ReleaseComObject()を呼び出すことによって彼らを決して二度としない土地に導きます。手動のメモリ管理。目に見えないインターフェイス参照を簡単に見逃してしまうため、正しく機能することはほとんどありません。GC.Collect()は実際に機能しますが、アプリをデバッグするときは機能しません。


1
ハンスが私のためにうまく答えた私の質問も見てください。stackoverflow.com/questions/15561025/...
デイブいや

1
:@HansPassant私はちょうどこの素晴らしいまた、ここでは私の質問の一部に答える説明、見つけstackoverflow.com/questions/30529379/... GCおよびスレッドの同期についてを。私がまだ持っている1つの質問:GCが実際にレジスタで使用されているアドレスを圧縮および更新しているのか(サスペンド中にメモリに格納されているのか)、それとも単にスキップするのでしょうか?スレッドを中断した後(再開前)にレジスタを更新するプロセスは、OSによってブロックされている深刻なセキュリティスレッドのように感じます。
2015年

間接的に、はい。スレッドは中断され、GCはCPUレジスタのバッキングストアを更新します。スレッドは実行を再開すると、更新されたレジスタ値を使用します。
ハンスパッサント2015年

1
@HansPassant、ここで説明したCLRガベージコレクターの非自明な詳細の参照を追加していただければ幸いです。
denfromufa 2017

これは重要な点は、(その「最適化コード」で、賢明なその構成を思わ<Optimize>true</Optimize>.csproj)有効になっています。これは、「リリース」構成のデフォルトです。ただし、カスタム構成を使用する場合は、この設定が重要であることを知っておくことが重要です。
Zero3

34

[ファイナライズプロセスの内部についてさらに追加したかっただけ]

したがって、オブジェクトを作成し、オブジェクトが収集されFinalizeたら、オブジェクトのメソッドを呼び出す必要があります。しかし、ファイナライズには、この非常に単純な仮定以外にもあります。

短い概念::

  1. オブジェクトがFinalizeメソッドを実装していない場合、メモリはすぐに再利用され
    ます。もちろん、アプリケーションコードからアクセスすることはできません

  2. 実装するオブジェクトFinalizeメソッドのコンセプト/実装はApplication RootsFinalization QueueFreacheable Queue彼らは再利用することができます前に来ます。

  3. アプリケーションコードで到達できないオブジェクトはゴミと見なされます

前提::クラス/オブジェクトA、B、D、G、HはFinalizeメソッドを実装せず、C、E、F、I、JはFinalizeメソッドを実装します。

アプリケーションが新しいオブジェクトを作成すると、新しい演算子はヒープからメモリを割り当てます。オブジェクトのタイプにFinalizeメソッドが含まれている場合、オブジェクトへのポインタがファイナライズキューに置かれます

したがって、オブジェクトC、E、F、I、Jへのポインタがファイナライズキューに追加されます。ファイナライズキューは、ガベージコレクタによって制御される内部データ構造です。キュー内の各エントリは、オブジェクトのメモリを解放する前にメソッドを呼び出す必要があるオブジェクトを指します。以下の図は、いくつかのオブジェクトを含むヒープを示しています。これらのオブジェクトのいくつかは、アプリケーションのルートから到達可能です

Finalize、そうでないものもあります。オブジェクトC、E、F、I、およびJが作成されると、.Netフレームワークは、これらのオブジェクトにFinalizeメソッドがあり、これらのオブジェクトへのポインターがファイナライズキューに追加されることを検出します

ここに画像の説明を入力してください

GCが発生すると(最初のコレクション)、オブジェクトB、E、G、H、I、およびJはガベージであると判断されます。A、C、D、Fは、上の黄色のボックスからの矢印で描かれたアプリケーションコードから到達可能であるためです。

ガベージコレクターは、ファイナライズキューをスキャンして、これらのオブジェクトへのポインターを探します。ポインターが見つかると、そのポインターはファイナライズキューから削除され、フリーチ可能なキュー(「F到達可能」)に追加されますfreachableキューは、ガベージコレクタによって制御される別の内部データ構造です。フリーチ可能なキューの各ポインターは、メソッドを呼び出す準備ができているオブジェクトを識別します。

Finalize

コレクション(最初のコレクション)の後、マネージヒープは次の図のようになります。以下の説明::
1.)オブジェクトB、G、およびHが占有するメモリは、呼び出す必要のあるfinalizeメソッドがなかったため、すぐに再利用されました

2.) ただし、オブジェクトE、I、およびJが占有するメモリは、Finalizeメソッドがまだ呼び出されていないため、再利用できませんでした。 Finalizeメソッドの呼び出しは、到達可能なキューによって行われます。

3.) A、C、D、Fは、上の黄色のボックスから矢印で描かれたアプリケーションコードで到達可能であるため、いかなる場合でも収集されません。

ここに画像の説明を入力してください

Finalizeメソッドの呼び出し専用の特別なランタイムスレッドがあります。フリーチ可能なキューが空の場合(通常はそうです)、このスレッドはスリープします。ただし、エントリが表示されると、このスレッドはウェイクし、キューから各エントリを削除して、各オブジェクトのFinalizeメソッドを呼び出します。ガベージコレクターは再利用可能なメモリを圧縮し、特別なランタイムスレッドは、各オブジェクトのメソッドを実行して、フリーチ可能なキューを空にしますFinalizeだから、最後にあなたのFinalizeメソッドが実行されるときです

次にガベージコレクターが呼び出されたとき(2番目のコレクション)、アプリケーションのルートがそれを指していなくて、フリーチ可能なキューがそれを指していません(EMPTYも)ので、完成したオブジェクトは本当にガベージであることがわかります。したがって、オブジェクト(E、I、J)のメモリは単にヒープから解放されます。下の図を参照して、上の図と比較してください

ここに画像の説明を入力してください

ここで理解しておくべき重要なことは、ファイナライズが必要なオブジェクトが使用するメモリを再利用するには、2つのGCが必要であることです。実際には、これらのオブジェクトは古い世代に昇格される可能性があるため、3つ以上のコレクションが必要になる場合もあります。

注:: freachableキューはグローバルと静的変数が根で同じようにルートであると考えられています。したがって、オブジェクトがフリーチ可能なキューにある場合、そのオブジェクトは到達可能であり、ガベージではありません。

最後の注意として、アプリケーションのデバッグは1つのことであり、ガベージコレクションは別のものであり、動作が異なることを覚えておいてください。これまでのところ、アプリケーションをデバッグするだけではガベージコレクションを感じることができません。さらに、メモリを調査したい場合は、ここから始めてください。

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