1. ホーム
  2. c#

[解決済み] .NETのガベージコレクションを理解する

2022-04-13 18:55:19

質問

以下のコードを考えてみてください。

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() が呼び出されたのに、なぜそこでファイナライズされないのでしょうか?

どのように解決するのですか?

デバッガを使用しているため、ここでつまづき、非常に間違った結論を導いています。 あなたのユーザーのマシンで実行される方法でコードを実行する必要があります。 Build + Configuration managerでまずReleaseビルドに切り替え、左上隅の"Active solution configuration"のコンボを"Release"に変更します。 次に、ツール + オプション、デバッグ、一般に進み、"JIT最適化を抑制する"オプションのチェックをはずします。

ここでもう一度プログラムを実行し、ソースコードをいじってみてください。 余分な中括弧は全く効果がないことに注意してください。 また、変数をnullに設定しても、全く変化がないことに注意してください。 常に"1"と表示されるのです。 これで、あなたが期待したとおりの動作をするようになりました。

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

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

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

テーブルのquot;no longer used"のアドレスは非常に重要です。 これは、ガベージコレクタを非常に 効率的 . オブジェクトの参照は、たとえそれがメソッド内で使われていて、そのメソッドがまだ実行を終了していなくても収集することができます。 たとえば Main() メソッドは、プログラムが終了する直前に実行を停止します。 たとえば Main() メソッドは、プログラムが終了する直前に実行を停止します。Main() メソッドの内部で使用されているオブジェクト参照が、プログラムの実行時間中ずっと生き続けることは明らかに好ましくありません。 ジッターは、Main()メソッドを呼び出す前にプログラムがどの程度進行したかによって、そのようなローカル変数がもはや有用でないことを発見するためにテーブルを使用することができます。

そのテーブルに関連する、ほとんど魔法のようなメソッドがGC.KeepAlive()です。 これは 非常に 特殊なメソッドで、コードは一切生成されません。 その唯一の義務は、そのテーブルを修正することです。 それは が拡張されます。 ローカル変数の寿命は、それが格納する参照がガベージコレクションされるのを防ぐ。 これを使う必要があるのは、GCが過剰に参照を収集するのを止めるときだけです。これは、相互運用のシナリオで、参照が管理されていないコードに渡されたときに起こります。 ガベージコレクタは、そのようなコードによって使われる参照を見ることができません。なぜなら、そのコードはジッタによってコンパイルされていないので、どこで参照を探すかというテーブルを持っていないのです。 EnumWindows() のようなアンマネージド関数にデリゲートオブジェクトを渡すことは、GC.KeepAlive() を使う必要がある場合の定型的な例と言えます。

つまり、Releaseビルドで実行した後のサンプルスニペットからわかるように、ローカル変数 よろしい は、メソッドの実行が終了する前に、早期に収集されます。 さらに強力なのは、オブジェクトのメソッドの1つが実行されている間でも、そのメソッドがもはや この . これには問題があって、そのようなメソッドをデバッグするのは非常に厄介です。 ウォッチウィンドウに変数を置いたり、インスペクションしたりすることはよくあることですから。 そして、それは 消える デバッグ中にGCが発生した場合。 それは非常に不愉快なので、ジッターは 意識する デバッガが接続されていること そして を変更します。 テーブルを変更し、最後に使用されたアドレスを変更します。 そして、通常の値から、メソッド内の最後の命令のアドレスに変更します。 これで、メソッドが戻ってこない限り、変数は生き続けることになります。 これにより、メソッドが戻るまで、その変数を監視し続けることができます。

これで、先ほど見たことと、なぜ質問したのかも説明できます。 GC.Collectの呼び出しが参照を収集できないため、"0"と表示されます。 テーブルには、変数が使用中であることが書かれています。 過去 GC.Collect()呼び出し、メソッドの終わりまでずっと。 デバッガが装着されているため、そう言わざるを得ません。 デバッグビルドを実行することで

変数をnullに設定することは、GCが変数を検査し、もはや参照を見ないので、今効果があります。 しかし、多くのC#プログラマーが陥っている罠にはまらないように、実際にそのコードを書くのは無意味なことです。 Releaseビルドでコードを実行するときに、その文があるかないかは、全く意味がありません。 実際、ジッターオプティマイザは 削除 という文がありますが、これは何の効果もないためです。 ですから、このようなコードを書かないように注意してください。 と思われる が効果を発揮します。


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