web-dev-qa-db-ja.com

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

以下のコードを検討してください。

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();
    }
}

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

158

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

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

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

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

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

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

そのテーブルに関連するほとんど魔法のメソッドはGC.KeepAlive()です。これはvery特別なメソッドであり、コードをまったく生成しません。その唯一の義務は、そのテーブルを変更することです。ローカル変数のライフタイムをextendsして、保存する参照がガベージコレクションされないようにします。使用する必要があるのは、参照がアンマネージコードに渡される相互運用シナリオで発生する可能性がある、参照の収集でGCが過度に熱心になるのを防ぐことだけです。ガベージコレクターは、ジッターによってコンパイルされていないため、そのようなコードで使用されている参照を見ることができません。したがって、参照を探す場所を示すテーブルがありません。デリゲートオブジェクトをEnumWindows()などのアンマネージ関数に渡すことは、GC.KeepAlive()を使用する必要がある場合の定型的な例です。

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

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

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


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

329
Hans Passant

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

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

短い概念::

  1. Finalizeメソッドを実装していないオブジェクトは、メモリはすぐに再生されますが、もちろん到達可能ではありません
    アプリケーションコードはもう

  2. Finalizeメソッドを実装するオブジェクト、Application RootsFinalization QueueFreacheable Queueの概念/実装は、それらが回収される前に来ます。

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

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

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

したがって、オブジェクトC、E、F、I、Jへのポインターがファイナライズキューに追加されます。

finalization queueは、ガベージコレクターによって制御される内部データ構造です。キュー内の各エントリは、オブジェクトのメモリを回収する前にFinalizeメソッドを呼び出す必要があるオブジェクトを指します。次の図は、いくつかのオブジェクトを含むヒープを示しています。これらのオブジェクトには、アプリケーションのルートから到達できるものとそうでないものがあります。オブジェクトC、E、F、I、およびJが作成されると、.NetフレームワークはこれらのオブジェクトにFinalizeメソッドがあり、これらのオブジェクトへのポインターが最終化キュー

enter image description here

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

ガベージコレクターは、finalization queueをスキャンして、これらのオブジェクトへのポインターを探します。 ポインタが見つかると、ポインタはファイナライズキューから削除され、到達可能キューに追加されます( "F-reachable")。

freachable queueは、ガベージコレクターによって制御される別の内部データ構造です。 freachable queue内の各ポインターは、Finalizeを持つ準備ができているオブジェクトを識別しますメソッドが呼び出されました。

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

2.)ただし、オブジェクトE、I、およびJが占有しているメモリは、Finalizeメソッドがまだ呼び出されていないため、回収できませんでした。Finalizeメソッドの呼び出しfreacheable queueによって行われます。

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

enter image description here

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

次回ガベージコレクターが呼び出されると(2nd Collection)、アプリケーションのルートがそれを指しておらず、freachable queuenoより長いポイント(それも空です)、したがって、オブジェクト(E、I、J)のメモリはヒープから再利用されます。下の図を参照し、上の図と比較してください。

enter image description here

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

注::freachable queueはグローバル変数や静的変数がルートであるように、ルートと見なされます。したがって、オブジェクトが到達可能キューにある場合、オブジェクトは到達可能であり、ゴミではありません。

最後の注意点として、アプリケーションのデバッグは別の問題であり、ガベージコレクションは別の問題であり、動作が異なることを忘れないでください。これまでは、アプリケーションをデバッグするだけではガベージコレクションを実行できません。さらに、Memory get ここから開始 を調査する場合は、

31
R.C

メモリ管理を実装できる方法は3つあります。

GCはマネージリソースに対してのみ機能するため、.NETはDisposeおよびFinalizeを提供して、ストリーム、データベース接続、COMオブジェクトなどのアンマネージリソースを解放します。

1)廃棄する

Disisは、IDisposableを実装する型に対して明示的に呼び出す必要があります。

プログラマは、Dispose()を使用するか、構造を使用してこれを呼び出す必要があります。

既にdispose()を使用している場合は、GC.SuppressFinalize(this)を使用してファイナライザーへの呼び出しを防止します

2)ファイナライズまたはディストラクタ

オブジェクトがクリーンアップの対象になった後に暗黙的に呼び出され、オブジェクトのファイナライザーはファイナライザースレッドによって順次呼び出されます。

ファイナライザを実装することの欠点は、そのようなクラス/タイプのファイナライザを事前クリーンアップと呼ばなければならないため、メモリの再利用が遅れるということです。

3)GC.Collect()

GC.Collect()を使用しても、コレクションにGCを配置する必要はありません。GCは必要に応じていつでもオーバーライドおよび実行できます。

また、GC.Collect()は、ガベージコレクションのトレース部分のみを実行し、アイテムをファイナライザーキューに追加しますが、別のスレッドによって処理されるタイプのファイナライザーを呼び出しません。

GC.Collect()を呼び出した後にすべてのファイナライザが呼び出されるようにする場合は、WaitForPendingFinalizersを使用します。

2
Pankaj Singh