C#の興味深い瞬間パート2

この記事では、.Netでメモリ内のオブジェクトを表すいくつかの機能、コンパイラーによって実行される最適化について説明しこの記事を書いたmynameco同志の伝統を続けたいと思い ます。



この投稿はkulhackersに焦点を当てていないため、列挙子のDispose呼び出しデザインでコンパイルを使用し、foreach演算子が機能するためにインターフェイスを使用する必要がないことがわかっている場合は、正しい署名の列挙子を返すGetEnumeratorメソッドが必要であり、Enumerator自体は可変(可変) )予期しないバグを引き起こす可能性のある構造...その後、読み込めず、時間を節約し、「KG \ AM」、「Boyans」、「Captains otake」などの投稿を残さないでください。 残りは猫の下でお願いします。







.Netのメモリ管理





一般に、このトピックについては多くのことが書かれていますので、これについて簡単に説明しましょう。



アプリケーションが起動した直後に、いわゆるGCルートが作成されます。 ルートは、次のようなオブジェクトのグラフを作成するために必要です。



画像



原則として、実行スレッドと並行する別のスレッドに組み込まれます。 さらに、このスレッドは、GCルートから到達できないオブジェクトをマークします(オブジェクトが現時点でルートからの参照を持たない場合、将来誰もそれを参照できなくなります)(図のノード9および10)。 プログラムが占有するメモリが特定の制限を超えるか、 GC.Collectメソッドが呼び出されると、ガベージコレクション自体が開始されます。



詳細については、この記事を参照してください



なんでこんなこと?



しかし、何に。 事実は

オブジェクトは、このオブジェクトが呼び出すメソッドを実行する前に削除できます。


このような単純なクラスがあるとします:



public class SomeClass { public int I; public SomeClass(int input) { I = input; Console.WriteLine("I = {0}", I); } ~SomeClass() { Console.WriteLine("deleted"); } public void Foo() { Thread.Sleep(1000); Console.WriteLine("Foo"); } }
      
      







次のように呼び出します。



 public class Program { private static void Main() { new Thread(() => { Thread.Sleep(100); GC.Collect(); }) {IsBackground = true}.Start(); new SomeClass(10).Foo(); } }}
      
      







コレクターが可能になった直後にオブジェクトを削除するために、スレッドが必要です。 現実には、GCはもちろん触れないほうがいいです。GCが動作するヒューリスティックは非常に愚かな人々によって開発されたものであり、 GC.Collectメソッドの手動呼び出しはアーキテクチャの問題を害するだけです。



このプログラムを実行して、この出力を取得します(コンパイラを最適化するにはリリースモードでコンパイルする必要があります)。

画像

もちろん、実際にはこれを取得するのは困難です。CLR(まあ、またはDLR)は、このメソッドが実行された時点でビルドすることを決定する必要がありますが、仕様によるとかなり可能です!



インスタンスメソッドは静的メソッドと違いはありませんが、隠されたパラメーターが渡されることを除いて機能します。thisリンクは、メソッドパラメーターの残りの部分よりも優れています。 さて、CILの観点からはまだわずかな違いがあります(インスタンスメソッドは、静的メソッドに単純な呼び出しが使用されたときに仮想としてマークされていないものも含め 、常にcallvirtを使用して呼び出されます)。



デストラクタの機能


C#のデストラクタ:真実か神話か?


はい、いいえ。 一方では、外観はC ++デストラクタに非常に似ています(同じアイコン-チルダで記述されています)が、実際にはObjectクラスから継承されたFinalizeメソッドをオーバーライドします(ところで、デストラクタにはpublic修飾子がありません、 プライベートまたはその他)。 したがって、C#のデストラクタは、ファイナライザと呼ばれることがよくあります。



聴衆からの質問:C ++デストラクタより悪くない素晴らしいツールがC#にあるのに、なぜDisposeに悩まされるのですか?



アルメニアのラジオは答えています:問題はコレクターにあります。 実際には、ファイナライザーが存在するため、オブジェクトは2つのパスで削除する必要があります。最初に、ファイナライザーが(別のスレッドで)実行し、次のガベージコレクションでのみ、オブジェクトが占有しているメモリを解放できます。 このため、オブジェクトはいわゆる「ミッドライフクライシス」によって特徴付けられます。多くのオブジェクトが第2(最終)世代に分類されると、結果として、コレクターは非常に遅くなります。 ゼロ世代のみを標準的にクリーニングする代わりに、彼はすべてのメモリを見ることを余儀なくされています。 説明は完全なふりをしていませんが、上記のリンクからこれらの質問はかなりうまく整理されており、キャプテンの啓示を含む長いシートは誰にも好まれることはめったにありません。



したがって、IDisposableインターフェイスを実装するすべてのオブジェクトでDisposeメソッドを呼び出すことをお勧めします。 念のため、標準ライブラリのほとんどのクラスには、次のようなファイナライザ(「バカに対する保護」)があります。



  public virtual void Close() //    Stream { this.Dispose(true); GC.SuppressFinalize((object) this); } public void Dispose() { this.Close(); } /// <summary> /// ,        ,     FileStream. /// </summary> [SecuritySafeCritical] ~FileStream() { if (this._handle == null) return; this.Dispose(false); }
      
      







そして、なぜこれすべてですか?


そして、ファイナライザの構造に関する知識を身につけて、オブジェクトを完全に「復活」させることができます!



ファイナライザは次のように書き直します。

  ~SomeClass() { Console.WriteLine("deleted"); Program.SomeClassInstance = this; GC.ReRegisterForFinalize(this); }
      
      







プログラムを少し変更します

  public class Program { public static SomeClass SomeClassInstance; private static void Main() { new Thread(() => { Thread.Sleep(100); GC.Collect(); }) {IsBackground = true}.Start(); var wr = new WeakReference(new SomeClass(10)); Console.WriteLine("IsAlive = {0}", wr.IsAlive); ((SomeClass)wr.Target).Foo(); Console.WriteLine("IsAlive = {0}", wr.IsAlive); } }
      
      







さて、開始後、オブジェクトは永久に削除され、復活し、再び削除されるなど、無限に印刷された行が削除されます... (スケルトンキングのように3チャージのイージス:))



画像



ファイナライザーが呼び出されることはありません




まあ、最も簡単なことは、プログラムがタスクマネージャーを介して、または別の非正規の方法(IDisposable piggy bankの別の方法)で強制的に閉じられる場合です。 しかし、他のどのような場合に呼び出されないのでしょうか?
答え
たとえば、プログラムが初めて起動され、オブジェクトが削除されなかった場合、ファイナライザーメソッドの代わりにスタブメソッドがあります(メソッドの本体のコンパイルが発生します)。 その後、ある瞬間にシステムに十分なメモリがなくクラッシュすると、他の同様の場合とは異なり、ファイナライザはスタブの代わりにこのファイナライザのコードを配置するためのメモリがないため、ファイナライザは呼び出されません。 さらに、各オブジェクトのファイナライザーは2秒以内に完了することができ、ファイナライズキュー全体に与えられる時間は40秒以内です。



追加する

以下に正しく記載されているように、CriticalFinalizerObjectから継承できます。 しかし、C#には多重継承がないため(インターフェイスは「実装」され、継承されません)、これは万能薬ではありません。 もちろん、そのような状況はまれなケースですが、メモリリークがない限り、理論の観点から考えると、原則として可能または不可能です。




結論の代わりに





一方では、このすべてを要約する必要があります。他方では、すべてが言われたようです。 一方では、興味深い可能性について書き込もうとしましたが、他方では、他の記事ですでに非常によく説明されているトピックについてはあまりキャプテンしませんでした。 何が起こったのか、あなたが決める。



ファイナライザーの使用に関する小さなヒント:





まあ、同志マヨロフが正しく言ったように

管理されていないリソースは、SafeHandleまたはCriticalHandleでラップする必要があります。 これにより、アンマネージリソースの初期化中にAppDomainをアンロードする必要がなくなります。



そして、安全なラッパーを介してアンマネージリソースを使用するクラスにはファイナライザーは必要ありません。結局、これらのラッパーには既にファイナライザーが含まれています。




PSセマンティック/スペル/句読点/構文/セマンティック/形態学的およびその他のエラーに関するコメントは受け付けます。



All Articles