191

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

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()そこでファイナライズされないのはなぜでしょうか?

4

3 に答える 3

379

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

もう一度プログラムを実行して、ソース コードをいじってみましょう。追加のブレースがまったく効果がないことに注意してください。また、変数を null に設定してもまったく違いがないことに注意してください。常に「1」が出力されます。これで、期待どおりに動作し、期待どおりに動作するようになりました。

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

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

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

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

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

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

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

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


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

于 2013-06-16T08:16:32.057 に答える