ホームページ  >  記事  >  バックエンド開発  >  .Netガベージコレクションの仕組み原理(1)

.Netガベージコレクションの仕組み原理(1)

黄舟
黄舟オリジナル
2017-02-17 11:21:291150ブラウズ

英語原文:Jeffrey Richter

編者:Zhao Yukai

リンク:http://www.php.cn/

Microsoft.Net clr のガベージ コレクション メカニズムを使用すると、プログラマはメモリを解放するタイミングに注意を払う必要がなくなります。メモリの解放は GC によって完全に行われ、プログラマにとっては透過的です。それにもかかわらず、.Net プログラマーとして、ガベージ コレクションがどのように機能するかを理解する必要があります。この記事では、.Net がマネージド メモリをどのように割り当てて管理するかを見てから、ガベージ コレクターのアルゴリズム メカニズムを段階的に説明します。
プログラムに適切なメモリ管理戦略を設計することは難しくて面倒です。また、この作業により、プログラム自体が解決すべき問題の解決に集中できなくなります。開発者がメモリ管理の問題を解決するのに役立つ組み込みメソッドはありますか?もちろん、.NetのGC、ガベージコレクションです。
考えてみましょう。すべてのプログラムは、画面表示、ネットワーク接続、データベース リソースなどのメモリ リソースを使用します。実際、オブジェクト指向環境では、各型はデータを保存するためにいくつかのメモリ リソースを占有する必要があります。オブジェクトは次の手順に従ってメモリを使用する必要があります:
1. 型にメモリ領域を割り当てる
2.メモリを初期化し、メモリを使用可能な状態に設定します
3. オブジェクトのメンバーにアクセスします
5. メモリを解放します
この一見単純なメモリ使用パターンにより、プログラミング上で多くの問題が発生することがあります。プログラマーは、使用されなくなったオブジェクトを解放し忘れたり、すでに解放されたオブジェクトにアクセスしようとしたりすることがあります。これら 2 種類のバグは通常、ある程度隠されており、論理的なエラーとは異なり、発見すると修正できます。プログラムがしばらく実行されていると、メモリ リークが発生し、予期しないクラッシュが発生する可能性があります。実際、開発者がメモリの問題を検出するのに役立つツールが数多くあります。たとえば、タスク マネージャー、システムなどです。 AcitvieX Control と Rational の Purify を監視します。
そして GC では、開発者がメモリを解放するタイミングに注意を払う必要はありません。ただし、ガベージ コレクターはメモリ内のすべてのリソースを管理できるわけではありません。ガベージ コレクターは、一部のリソースをリサイクルする方法を知りません。開発者は、それらをリサイクルするための独自のコードを作成する必要があります。 .Net で フレームワークでは、開発者は通常、Close、Dispose、または Finalize メソッドにコードを記述します。このメソッドはガベージ コレクターによって自動的に呼び出されます。
ただし、Rectangle など、リソースを解放するためのコードを実装する必要のないオブジェクトも多くあります。これをクリアするには、ガベージ コレクターで行うことができます。これ。オブジェクトにメモリがどのように割り当てられるかを見てみましょう。
オブジェクトの割り当て:

.Net clr は、すべての参照オブジェクトをマネージド ヒープに割り当てます。これは C ランタイム ヒープに非常に似ていますが、オブジェクトを解放するタイミングに注意を払う必要はありません。オブジェクトは使用されないときに自動的に解放されます。このようにして、ガベージ コレクターは、オブジェクトがもう使用されておらず、リサイクルする必要があることをどのようにして知るのでしょうか?という疑問が生じます。これについては後で説明します。
現在、ガベージ コレクション アルゴリズムがいくつかあり、それぞれが特定の環境向けにパフォーマンスを最適化します。この記事では、clr ガベージ コレクション アルゴリズムに焦点を当てます。基本的な概念から始めましょう。
プロセスが初期化されると、実行時に連続的な空のメモリ領域が予約されます。このメモリ領域はマネージド ヒープです。マネージド ヒープは、次のオブジェクトの割り当てアドレスを指すポインター (NextObjPtr と呼びます) を記録します。最初、このポインターはマネージド ヒープの開始位置を指します。
アプリケーションは new 演算子を使用して新しいオブジェクトを作成します。この演算子は、マネージド ヒープの残りの領域がオブジェクトを収容できることを最初に確認し、オブジェクトを指すように NextObjPtr を呼び出します。関数、new 演算子はオブジェクトのアドレスを返します。


図 1 マネージド ヒープ

このとき、NextObjPtr は、マネージド ヒープ上で次のオブジェクトが割り当てられる場所を指します。図 1 は、マネージド ヒープ内に 3 つのオブジェクト A、B、C があることを示しています。次のオブジェクトは、NextObjPtr が指す場所 (C オブジェクトの隣) に配置されます。
次に、C ランタイム ヒープがメモリを割り当てる方法を見てみましょう。 C ランタイム ヒープでは、メモリを割り当てるには、十分な大きさのメモリ ブロックが見つかるまでリンク リストのデータ構造をたどる必要があります。分割後、リンク リスト内のポインタは残りのメモリ領域を指す必要があります。リンクされたリストが損なわれていないことを確認してください。マネージド ヒープの場合、オブジェクトの割り当ては NextObjPtr ポインターのポインターのみを変更するため、非常に高速です。実際、マネージド ヒープにオブジェクトを割り当てることは、スレッド スタックにメモリを割り当てることに非常に似ています。
これまでのところ、マネージド ヒープにメモリを割り当てる方が、C ランタイム ヒープにメモリを割り当てるよりも高速かつ簡単に実装できるようです。もちろん、マネージド ヒープはアドレス空間が無制限であると仮定しているため、この利点が得られます。明らかにこの仮定は間違っています。この仮定が正しいことを確認するメカニズムが必要です。このメカニズムがガベージ コレクターです。どのように機能するかを見てみましょう。
アプリケーションがオブジェクトを作成するために new オペレーターを呼び出すとき、オブジェクトを保存するためのメモリーがない可能性があります。マネージド ヒープは、NextObjPtr が指す領域がヒープのサイズを超えているかどうかを検出できます。ヒープのサイズを超えている場合は、マネージド ヒープがいっぱいであり、ガベージ コレクションが必要であることを意味します。
実際には、ジェネレーション 0 ヒープがいっぱいになった後にガベージ コレクションがトリガーされます。 「生成」は、ガベージ コレクターがパフォーマンスを向上させるための実装メカニズムです。 「世代」とは、新しく作成されたオブジェクトが若い世代であり、リサイクル操作が行われる前にリサイクルされなかったオブジェクトが古いオブジェクトであることを意味します。オブジェクトを世代に分割すると、ガベージ コレクターはすべてのオブジェクトを収集するのではなく、特定の世代のオブジェクトのみを収集できるようになります。

ガベージ コレクション アルゴリズム:

ガベージ コレクターは、アプリケーションによって使用されなくなったオブジェクトがあるかどうかを確認します。このようなオブジェクトが存在する場合、これらのオブジェクトが占有しているスペースを再利用できます (ヒープ上に十分なメモリが存在しない場合、new オペレータは OutofMemoryException をスローします)。オブジェクトがまだ使用されているかどうかをガベージ コレクターがどのように判断するのか疑問に思われるかもしれません。この質問に答えるのは簡単ではありません。
すべてのアプリケーションにはルート オブジェクトのセットがあります。ルートは、マネージド ヒープ上のアドレスを指す場合もあれば、null である場合もあります。たとえば、すべてのグローバルおよび静的オブジェクト ポインターはアプリケーションのルート オブジェクトです。さらに、スレッド スタック上のローカル変数/パラメーターもアプリケーションのルート オブジェクトであり、マネージド ヒープを指す CPU レジスタ内のオブジェクトもルート オブジェクトです。 。生き残ったルート オブジェクトのリストは JIT (ジャストインタイム) コンパイラーと clr によって維持され、ガベージ コレクターはこれらのルート オブジェクトにアクセスできます。
ガベージ コレクターが実行を開始すると、マネージド ヒープ上のすべてのオブジェクトがガベージであると想定されます。つまり、ルート オブジェクトも、ルート オブジェクトによって参照されるオブジェクトも存在しないと想定されます。次に、ガベージ コレクターはルート オブジェクトの走査を開始し、ルート オブジェクトへの参照を持つすべてのオブジェクトのグラフを構築します。
図 2 は、マネージド ヒープ上のアプリケーションのルート オブジェクトが A、C、D、F であることを示しています。これらのオブジェクトはグラフの一部であり、オブジェクト D がオブジェクト H を参照し、オブジェクト H もグラフに追加されます。ガベージ コレクターは、到達可能なすべてのオブジェクトをループします。


写真2 マネージド ヒープ上のオブジェクト

ガベージ コレクターは、ルート オブジェクトと参照オブジェクトを 1 つずつ走査します。ガベージ コレクターは、オブジェクトが既にグラフ内に存在することを検出すると、パスを変更し、そのトラバースを継続します。これには 2 つの目的があります。1 つはパフォーマンスを向上させること、もう 1 つは無限ループを回避することです。
すべてのルート オブジェクトがチェックされると、ガベージ コレクターのグラフには、アプリケーション内の到達可能なすべてのオブジェクトが含まれます。このグラフにないマネージド ヒープ上のオブジェクトはすべて、リサイクルされるガベージ オブジェクトです。到達可能なオブジェクト グラフを構築した後、ガベージ コレクターはマネージド ヒープを線形に走査し、連続するガベージ オブジェクトのブロック (空きメモリと見なすことができます) を見つけます。次に、ガベージ コレクターは非ガベージ オブジェクトをまとめて移動し (C の memcpy 関数を使用)、すべてのメモリ フラグメントをカバーします。もちろん、オブジェクトを移動するときはすべてのオブジェクト ポインターを無効にします (間違っている可能性があるため)。したがって、ガベージ コレクターは、アプリケーションのルート オブジェクトがオブジェクトの新しいメモリ アドレスを指すように変更する必要があります。さらに、オブジェクトに別のオブジェクトへのポインターが含まれている場合、ガベージ コレクターは参照を変更する責任もあります。図 3 は、コレクション後のマネージド ヒープを示しています。


写真3 リサイクル後のマネージド ヒープを図 3 に示します。リサイクル後、すべてのガベージ オブジェクトが識別され、すべての非ガベージ オブジェクトが一緒に移動されます。すべての非ガベージ オブジェクトのポインターも、移動されたメモリ アドレスに変更され、NextObjPtr は最後の非ガベージ オブジェクトの後ろを指します。現時点では、新しいオペレーターは引き続きオブジェクトを正常に作成できます。
ご覧のとおり、ガベージ コレクションには大幅なパフォーマンスの低下があり、これはマネージド ヒープを使用することの明らかな欠点です。 ただし、マネージド ヒープの速度が低下するまでメモリ再利用操作は実行されないことに注意してください。マネージド ヒープのパフォーマンスは、フルになるまでは c-runtime ヒープのパフォーマンスよりも優れています。ランタイム ガベージ コレクターはパフォーマンスの最適化も行います。これについては次の記事で説明します。
次のコードは、オブジェクトがどのように作成および管理されるかを示しています:

class Application {
public static int Main(String[] args) {
 
      // ArrayList object created in heap, myArray is now a root
      ArrayList myArray = new ArrayList();
 
      // Create 10000 objects in the heap
      for (int x = 0; x < 10000; x++) {
         myArray.Add(new Object());    // Object object created in heap
      }
 
      // Right now, myArray is a root (on the thread&#39;s stack). So, 
      // myArray is reachable and the 10000 objects it points to are also 
      // reachable.
      Console.WriteLine(a.Length);
 
      // After the last reference to myArray in the code, myArray is not 
      // a root.
      // Note that the method doesn&#39;t have to return, the JIT compiler 
      // knows
      // to make myArray not a root after the last reference to it in the 
      // code.
 
      // Since myArray is not a root, all 10001 objects are not reachable
      // and are considered garbage.  However, the objects are not 
      // collected until a GC is performed.
   }
}

もしかしたら、GC はとても優れているのに、なぜ ANSI C++ に含まれていないのかと疑問に思うかもしれません。 その理由は、ガベージ コレクターがアプリケーションのルート オブジェクト リストを見つけられ、オブジェクトのポインターを見つけられる必要があるためです。 C++ では、オブジェクト ポインターは相互に変換できますが、ポインターがどのオブジェクトを指しているかを知る方法はありません。 CLR では、マネージド ヒープがオブジェクトの実際の型を認識します。メタデータ情報を使用して、オブジェクトがどのメンバー オブジェクトを参照しているかを判断できます。

ガベージ コレクションとファイナライズ

ガベージ コレクターは、オブジェクトがガベージとしてマークされた後、その Finalize メソッドを自動的に呼び出すことができる追加機能を提供します (オブジェクトがオブジェクトの Finalize メソッドをオーバーライドする場合)。
Finalize メソッドはオブジェクト オブジェクトの仮想メソッドです。必要に応じてこのメソッドをオーバーライドできますが、このメソッドは C++ デストラクターと同様の方法でのみ書き換えることができます。例:

{
~Foo(){
        Console.WriteLine(“Foo Finalize”);
}
}

ここで C++ を使用したプログラマーは、Finalize メソッドが C++ のデストラクターとまったく同じように記述されているという事実に特に注意する必要があります。ただし、Finalize メソッドと .Net のデストラクターは異なります。破壊することはできず、ガベージ コレクションによってのみリサイクルできます。
クラスを設計するときは、次の理由から Finalize メソッドのオーバーライドを避けることが最善です:
1. Finalize を実装するオブジェクトは古い「世代」に昇格されるため、メモリ負荷が増大し、オブジェクトがこの世代に関連付けられます。オブジェクト オブジェクトは、初めてゴミになった時点ではリサイクルできません。
2. これらのオブジェクトの割り当て時間は長くなります。
3. ガベージ コレクターに Finalize メソッドを実行させると、パフォーマンスが大幅に低下します。 Finalize メソッドを実装するすべてのオブジェクトは、Finalize メソッドを実行する必要があることに注意してください。長さが 10000 の配列オブジェクトがある場合、各オブジェクトは Finalize メソッドを実行する必要があります。Finalize メソッドをオーバーライドするオブジェクトは、他のオブジェクトを参照する可能性があります。 Finalize メソッドを実装するオブジェクトも遅延リサイクルされます
5。Finalize メソッドがいつ実行されるかを制御する方法はありません。 Finalize メソッドでデータベース接続などのリソースを解放する場合、データベース リソースがかなり時間が経ってから解放される可能性があります。 6. プログラムがクラッシュしても、一部のオブジェクトはまだ参照されており、それらの Finalize メソッドには実行するチャンス。この状況は、オブジェクトがバックグラウンド スレッドで使用されるとき、オブジェクトがプログラムを終了するとき、または AppDomain がアンロードされるときに発生します。また、デフォルトではアプリケーションの強制終了時にFinalizeメソッドは実行されません。もちろん、オペレーティング システムのリソースはすべて再利用されますが、マネージド ヒープ上のオブジェクトは再利用されません。この動作は、GC の RequestFinalizeOnShutdown メソッドを呼び出すことで変更できます。
7. ランタイムは、複数のオブジェクトの Finalize メソッドが実行される順序を制御できません。場合によっては、オブジェクトの破棄が順次行われることがあります。定義したオブジェクトで Finalize メソッドを実装する必要がある場合は、Finalize メソッドができるだけ早く実行されるようにし、スレッド同期操作を含むブロックを引き起こす可能性のあるすべての操作を回避してください。さらに、Finalize メソッドによって例外が発生しないことを確認してください。例外が発生した場合、ガベージ コレクターは他のオブジェクトの Finalize メソッドを実行し続け、例外を直接無視します。
コンパイラーがコードを生成すると、コンストラクター上で基本クラスのコンストラクターが自動的に呼び出されます。同様に、C++ コンパイラは、デストラクターの基本クラス デストラクターへの呼び出しを自動的に追加します。ただし、.Net の Finalize 関数はそうではなく、コンパイラは Finalize メソッドに対して特別な処理を行いません。 Finalize メソッド内で親クラスの Finalize メソッドを呼び出したい場合は、呼び出しコードを自分で明示的に追加する必要があります。
C# の Finalize メソッドは C++ のデストラクターと同じように記述されていますが、C# はデストラクターをサポートしていないことに注意してください。この書き方に騙されないでください。

Finalize メソッドを呼び出す GC の内部実装

表面的には、ガベージ コレクターが Finalize メソッドを使用するのは非常に簡単です。オブジェクトを作成し、そのオブジェクトがリサイクルされるときにその Finalize メソッドを呼び出します。しかし、実際にはもう少し複雑です。
アプリケーションが新しいオブジェクトを作成すると、new オペレーターはヒープ上にメモリを割り当てます。オブジェクトが Finalize メソッドを実装している場合。オブジェクト ポインタはファイナライゼーション キューに置かれます。ファイナライゼーション キューは、ガベージ コレクターによって制御される内部データ構造です。キュー内の各オブジェクトは、リサイクル時に Finalize メソッドを呼び出す必要があります。
下の図に示すヒープには複数のオブジェクトが含まれており、その一部はオブジェクトであり、一部はオブジェクトではありません。オブジェクト C、E、F、I、J が作成されると、システムはこれらのオブジェクトが Finalize メソッドを実装していることを検出し、それらのポインターをファイナライズ キューに置きます。


Finalize メソッドが行うことは、通常、ガベージ コレクターがリサイクルできないリソース (ファイル ハンドル、データベース接続など) をリサイクルすることです。
ガベージが収集されると、オブジェクト B、E、G、H、I、および J がガベージとしてマークされました。ガベージ コレクターはファイナライゼーション キューをスキャンして、これらのオブジェクトへのポインターを見つけます。オブジェクト ポインタが見つかると、そのポインタは Feachable キューに移動されます。 Feachable キューは、ガベージ コレクターによって制御される別の内部データ構造です。 Feachable キュー内の各オブジェクトの Finalize メソッドが実行されます。
ガベージ コレクション後のマネージド ヒープは図 6 に示されています。オブジェクト B、G、および H は、Finalize メソッドを持たないため、リサイクルされたことがわかります。ただし、オブジェクト E、I、および J は、Finalize メソッドがまだ実行されていないため、まだリサイクルされていません。

写真5 ガベージコレクション後のマネージドヒープ


プログラムの実行中、Freatable キュー内のオブジェクトの Finalize メソッドの呼び出しを担当する専用のスレッドが存在します。 Feachable キューが空の場合、このスレッドはスリープ状態になります。キュー内にオブジェクトがあると、スレッドが起動され、キュー内のオブジェクトが削除され、Finalize メソッドが呼び出されます。したがって、Finalize メソッドを実行するときにスレッドのローカルにアクセスしようとしないでください。 ストレージ。
ファイナライゼーション キューと Freatable キューの間の相互作用は非常に巧妙です。まず、freatable という名前の由来をお話しましょう。 F は明らかにファイナライズです。このキュー内のすべてのオブジェクトが Finalize メソッドの実行を待っているということは、これらのオブジェクトが到着することを意味します。つまり、Feachable キュー内のオブジェクトは、グローバル変数や静的変数と同様に、フォロー オブジェクトとみなされます。したがって、オブジェクトが到達可能キュー内にある場合、そのオブジェクトはガベージではありません。
簡単に言うと、オブジェクトが到達不能な場合、ガベージ コレクターはそのオブジェクトをガベージであると見なします。その後、ガベージ コレクターがオブジェクトをファイナライズ キューから Feachable キューに移動すると、これらのオブジェクトはガベージではなくなり、メモリは再利用されなくなります。この時点で、ガベージ コレクターはガベージのマーク付けを完了し、ガベージとしてマークされた一部のオブジェクトは非ガベージ オブジェクトとして再検討されています。ガベージ コレクターは圧縮されたメモリを再利用し、到達可能なキューをクリアして、キュー内の各オブジェクトの Finalize メソッドを実行します。


図6 ガベージコレクションを再度実行した後のマネージドヒープ

再次出发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。

垃圾回收让对象复活

在前面部分我们已经说了,当程序不使用某个对象时,这个对象会被回收。然而,如果对象实现了Finalize方法,只有当对象的Finalize方法执行之后才会认为这个对象是可回收对象并真正回收其内存。换句话说,这类对象会先被标识为垃圾,然后放到freachable队列中复活,然后执行Finalize之后才被回收。正是Finalize方法的调用,让这种对象有机会复活,我们可以在Finalize方法中让某个对象强引用这个对象;那么垃圾回收器就认为这个对象不再是垃圾了,对象就复活了。
如下复活演示代码:

public class Foo {
~Foo(){
Application.ObjHolder = this;
  }
}
 
class Application{
  static public Object ObjHolder = null;
}

在这种情况下,当对象的Finalize方法执行之后,对象被Application的静态字段ObjHolder强引用,成为根对象。这个对象就复活了,而这个对象引用的对象也就复活了,但是这些对象的Finalize方法可能已经执行过了,可能会有意想不到的错误发生。
事实上,当你设计自己的类型时,对象的终结和复活有可能完全不可控制。这不是一个好现象;处理这种情况的常用做法是在类中定义一个bool变量来表示对象是否执行过了Finalize方法,如果执行过Finalize方法,再执行其他方法时就抛出异常。
现在,如果有其他的代码片段又将Application.ObjHolder设置为null,这个对象变成不可达对象。最终垃圾回收器会把对象当成垃圾并回收对象内存。请注意这一次对象不会出现在finalization队列中,它的Finalize方法也不会再执行了。
复活只有有限的几种用处,你应该尽可能避免使用复活。尽管如此,当使用复活时,最好重新将对象添加到终结队列中,GC提供了静态方法ReRegisterForFinalize方法做这件事:

如下代码:

public class Foo{
~Foo(){
Application.ObjHolder = this;
GC.ReRegisterForFinalize(this);
}
}

当对象复活时,重新将对象添加到复活队列中。需要注意的时如果一个对象已经在终结队列中,然后又调用了GC.ReRegisterForFinalize(obj)方法会导致此对象的Finalize方法重复执行。
垃圾回收机制的目的是为开发人员简化内存管理。
下一篇我们谈一下弱引用的作用,垃圾回收中的“代”,多线程中的垃圾回收和与垃圾回收相关的性能计数器。

 以上就是.Net 垃圾回收机制原理(一)的内容,更多相关内容请关注PHP中文网(www.php.cn)! 


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。