ガベージ コレクションの基礎

共通言語ランタイム (CLR) では、自動メモリ マネージャーとしてガベージ コレクター (GC) を使用できます。 ガベージ コレクターは、アプリケーションのメモリの割り当てと解放を管理します。 したがって、マネージド コードを扱う開発者は、メモリ管理タスクを実行するためのコードを記述する必要がありません。 自動メモリ管理により、オブジェクトを解放し忘れたためにメモリ リークが発生する、既に解放されているオブジェクトの解放されたメモリにアクセスしようとするなどの一般的な問題を回避できます。

この記事では、ガベージ コレクションの主要な概念について説明します。

利点

ガベージ コレクターには、次のような利点があります。

  • 開発者がメモリを手動で解放する必要がなくなります。

  • オブジェクトが効率的にマネージド ヒープに割り当てられます。

  • 使用されなくなったオブジェクトが解放され、メモリがクリアされてその後の割り当てに使用できるようになります。 マネージド オブジェクトは自動的にクリーンな内容で開始されるため、コンストラクターでデータ フィールドごとに初期化する必要はありません。

  • オブジェクトにおいて別のオブジェクトに割り当てられたメモリを決して使用できないようにすることで、メモリの安全性を確保します。

メモリの基礎

CLR のメモリに関する重要な概念の概要を以下に示します。

  • 各プロセスは、分離された独自の仮想アドレス空間を持ちます。 同じコンピューターのすべてのプロセスが同じ物理メモリとページ ファイル (存在する場合) を共有します。

  • 32 ビット コンピューターでは、各プロセスが既定で 2 GB のユーザー モード仮想アドレス空間を持ちます。

  • アプリケーション開発者が操作するのは仮想アドレス空間だけで、直接物理メモリを操作することはありません。 マネージド ヒープの仮想メモリの割り当てと解放はガベージ コレクターによって行われます。

    ネイティブ コードを記述する場合は、Windows 関数を使用して仮想アドレス空間を操作します。 ネイティブ ヒープの仮想メモリの割り当てと解放はこれらの関数によって行われます。

  • 仮想メモリには次の 3 つの状態があります。

    状態 説明
    Free 参照されていない、割り当てに使用できるメモリ ブロックです。
    予約されています。 使用できるように確保された、他の割り当て要求には使用できないメモリ ブロックです。 ただし、このメモリ ブロックがコミットされるまではデータを格納できません。
    Committed 物理ストレージに割り当てられたメモリ ブロックです。
  • 仮想アドレス空間は断片化される可能性があります。つまり、アドレス空間にはホールと呼ばれる空きブロックがあります。 仮想メモリの割り当てが要求された場合、仮想メモリ マネージャーは、その割り当て要求を満たすのに十分な大きさの単一の空きブロックを見つけなければなりません。 2 GB の空き領域があっても、そのすべての空き領域が 1 つのアドレス ブロックの中にない場合、2 GB の領域を必要とする割り当ては失敗します。

  • メモリが足りなくなるのは、予約する仮想アドレス空間が足りなくなった場合か、コミットする物理領域が足りなくなった場合です。

    ページ ファイルは、物理メモリの圧迫度 (物理メモリに対する需要) が低い場合にも使用されます。 最初に物理メモリの圧迫度が高まったときに、オペレーティング システムでは、データを格納するための領域の確保が必要となり、物理メモリのデータの一部がページ ファイルにバックアップされます。 そのデータは必要になるまでページングされないため、物理メモリの圧迫度が低い状況でページングが発生する可能性もあります。

メモリ割り当て

新しいプロセスが初期化されると、ランタイムは連続したアドレス空間領域をそのプロセスのために予約します。 この予約済みのアドレス空間をマネージド ヒープと呼びます。 マネージド ヒープは、ヒープ内で次のオブジェクトを割り当てるアドレスへのポインターを管理します。 初期状態では、このポインターはマネージド ヒープのベース アドレスに設定されます。 すべての参照型は、マネージド ヒープ上に割り当てられます。 アプリケーションが最初の参照型を作成すると、マネージド ヒープのベース アドレスの位置にその型のメモリが割り当てられます。 アプリケーションが次のオブジェクトを作成すると、ランタイムは、アドレス空間で最初のオブジェクトの直後のメモリをそのオブジェクトに割り当てます。 ランタイムは、使用できるアドレス空間がある限り、この方法で新しいオブジェクトにアドレス空間を割り当てていきます。

マネージド ヒープからのメモリ割り当ては、アンマネージド メモリ割り当てよりも高速に処理されます。 ランタイムはポインターに値を加算することによってオブジェクトにメモリを割り当てるため、これは、スタックからのメモリ割り当てとほとんど同じ速度になります。 また、連続して割り当てられた複数の新規オブジェクトは、マネージド ヒープに連続して格納されるため、アプリケーションからそれらのオブジェクトに高速でアクセスできます。

メモリの解放

ガベージ コレクターの最適化エンジンは、現在の割り当て状況に基づいて、ガベージ コレクションの実行に最適な時期を判断します。 ガベージ コレクターは、ガベージ コレクションを実行するときに、アプリケーションが使用しなくなったオブジェクトのメモリを解放します。 使用されなくなったオブジェクトを判断するために、アプリケーションの "ルート" を調べます。 アプリケーションのルートには、静的フィールド、スレッドのスタック上のローカル変数、CPU レジスタ、GC ハンドル、ファイナライズ キューが含まれています。 各ルートは、マネージド ヒープ上のオブジェクトを参照しているか、または null に設定されています。 ガベージ コレクターでこれらのルートのランタイムの残りを要求できます。 ガベージ コレクターでは、このリストを使用して、ルートから到達できるすべてのオブジェクトを含むグラフを作成します。

このグラフに含まれないオブジェクトは、アプリケーションのルートからは到達できません。 ガベージ コレクターは、到達できないオブジェクトをガベージであると判断し、それらに割り当てられたメモリを解放します。 ガベージ コレクション中に、ガベージ コレクターはマネージド ヒープを調べ、到達できないオブジェクトが占有しているアドレス空間ブロックを検索します。 到達できないオブジェクトを検出すると、それらのオブジェクトに割り当てられていたアドレス空間ブロックを解放し、メモリ コピー機能を使用して、到達できるオブジェクトのメモリを圧縮します。 到達できるオブジェクトのメモリを圧縮した後で、ガベージ コレクターは、アプリケーションのルートがそれらのオブジェクトの新しい位置を指すようにポインターを修正します。 また、マネージド ヒープのポインターも、最後の到達できるオブジェクトの後を指すように修正します。

メモリが圧縮されるのは、コレクション中に、大量の到達できないオブジェクトが検出された場合だけです。 マネージド ヒープ内のすべてのオブジェクトがごみではないと判断された場合は、メモリを圧縮する必要がありません。

パフォーマンスを向上させるために、ランタイムは、大きいオブジェクトのメモリは独立したヒープに割り当てます。 ガベージ コレクターは、これらの大きいオブジェクトのメモリを自動的に解放します。 ただし、メモリ内で大きいオブジェクトを移動するのを避けるため、通常このメモリは圧縮されません。

ガベージ コレクションの条件

ガベージ コレクションは、次のいずれかの条件に当てはまる場合に発生します。

  • システムの物理メモリが少ない場合。 メモリ サイズは、オペレーティング システムからのメモリ不足通知またはホストによって示されるメモリ不足のいずれかによって検出されます。

  • マネージド ヒープで割り当てられたオブジェクトによって使用されているメモリが、許容されるしきい値を超える場合。 このしきい値は、プロセスの進行に合わせて絶えず調整されます。

  • GC.Collect メソッドが呼び出されます。 ほとんどの場合、ガベージ コレクターは継続して実行されるため、このメソッドを呼び出す必要はありません。 このメソッドは、主に特別な状況やテストで使用されます。

マネージド ヒープ

CLR では、ガベージ コレクターを初期化してから、オブジェクトを格納および管理するためのメモリのセグメントを割り当てます。 オペレーティング システムのネイティブ ヒープに対し、このメモリのことをマネージド ヒープと呼びます。

マネージド ヒープはマネージド プロセスごとに割り当てられます。 プロセス内のすべてのスレッドは、同じヒープにオブジェクト用のメモリを割り当てます。

メモリを予約するために、ガベージ コレクターは Windows VirtualAlloc 関数を呼び出し、マネージド アプリケーション用のメモリのセグメントを一度に 1 つずつ予約します。 また、ガベージ コレクターは、必要に応じてセグメントを予約したり、Windows VirtualFree 関数を呼び出すことで (オブジェクトのセグメントをクリアしてから) セグメントを解放してオペレーティング システムに戻したりします。

重要

ガベージ コレクターによって割り当てらるセグメントのサイズは実装に固有であり、定期的な更新プログラムによる場合を含め、いつでも変更されることがあります。 アプリでは、セグメント サイズを推測することや、特定のセグメント サイズに依存することを絶対に避けてください。また、セグメントの割り当てに使用可能なメモリの量を構成しようとしてもなりません。

ヒープに割り当てられたオブジェクトが少ないほど、ガベージ コレクターの処理も少なくなります。 オブジェクトを割り当てるときは、必要以上に切り上げた値を使用しないでください。たとえば、15 バイトしか必要がないときに 32 バイトの配列を割り当てるなどです。

ガベージ コレクションがトリガーされると、ガベージ コレクターは、使用されなくなったオブジェクトに占有されているメモリを解放します。 この解放プロセスでは、まとめて移動できるように有効なオブジェクトを圧縮し、使用されなくなったスペースを削除することで、ヒープを小さくします。 このプロセスにより、一緒に割り当てられたオブジェクトが同じマネージド ヒープにまとめられ、局所性が保持されます。

ガベージ コレクションの割り込みの動作 (頻度と期間) は、割り当てのボリュームとマネージド ヒープ上の残ったメモリの量によって決まります。

ヒープは、大きなオブジェクト ヒープと小さなオブジェクト ヒープの 2 つを累積したものと見なすことができます。 大きなオブジェクト ヒープには、85,000 バイトを超えるオブジェクト (通常は配列) が格納されます。 インスタンス オブジェクトが極端に大きくなることはほとんどありません。

ヒント

オブジェクトのしきい値サイズを構成すれば、大きなオブジェクト ヒープに移ることができます。

ジェネレーション

GC アルゴリズムは、次のよういないくつかの考慮事項に基づいています。

  • マネージド ヒープ全体よりも、マネージド ヒープの一部のメモリを圧縮する方が高速です。
  • オブジェクトが新しいほどその存続期間は短く、オブジェクトが古いほど存続期間は長くなります。
  • 新しいオブジェクトは相互に関連し、アプリケーションからほぼ同時にアクセスされる傾向があります。

ガベージ コレクションは主に、有効期間が短いオブジェクトを解放する場合に発生します。 ガベージ コレクターのパフォーマンスを最適化するために、マネージド ヒープは 0、1、および 2 の 3 つのジェネレーションに分割されます。そのため、有効期間が長いオブジェクトと短いオブジェクトを別々に処理できます。 ガベージ コレクターは、新しいオブジェクトをジェネレーション 0 に格納します。 アプリケーションの有効期間の初期に作成され、ガベージ コレクションでごみではないと判断されたオブジェクトは昇格してジェネレーション 1 とジェネレーション 2 に格納されます。 マネージド ヒープの一部を圧縮する方がヒープ全体を圧縮するよりも高速であるため、この手法では、ガベージ コレクターがコレクションを実行するたびにマネージド ヒープ全体のメモリを解放するのではなく、特定のジェネレーションのメモリだけを解放できるようにします。

  • ジェネレーション 0: これは一番最初のジェネレーションで、有効期間が短いオブジェクトが格納されます。 有効期間が短いオブジェクトには、たとえば、テンポラリ変数などがあります。 ガベージ コレクションは、このジェネレーションで最も頻繁に発生します。

    新しく割り当てられたオブジェクトにより、新しいオブジェクトが生成されます。また、新しく割り当てられたオブジェクトは暗黙的にジェネレーション 0 コレクションになります。 ただし、大きなオブジェクトであれば、大きなオブジェクト ヒープ (LOH) に移されます。これは、"ジェネレーション 3" と呼ばれることもあります。 ジェネレーション 3 は、ジェネレーション 2 の一部として論理的に収集される、物理的なジェネレーションです。

    ジェネレーション 0 では、ほとんどのオブジェクトがガベージ コレクションで解放され、次のジェネレーションには残りません。

    ジェネレーション 0 がいっぱいになったときにアプリケーションによって新しいオブジェクトの作成が試みられると、そのオブジェクト用のアドレス空間を解放するために、ガベージ コレクターによってコレクションが実行されます。 まず、ガベージ コレクターは、マネージド ヒープ内の全オブジェクトではなく、ジェネレーション 0 のオブジェクトだけを調べます。 多くの場合、ジェネレーション 0 のコレクションを行うだけで、アプリケーションが新しいオブジェクトの作成を続行するために十分なメモリを解放できます。

  • ジェネレーション 1: このジェネレーションには有効期間が短いオブジェクトが格納されます。有効期間が短いオブジェクトと有効期間が長いオブジェクトの間のバッファーとして機能します。

    ガベージ コレクターは、ジェネレーション 0 のコレクションを実行した後で、到達できるオブジェクトのメモリを圧縮し、それらをジェネレーション 1 に昇格します。 一般にガベージ コレクションでごみだと判断されなかったオブジェクトの存続期間は長いので、これらのオブジェクトを上位のジェネレーションに昇格させるのは有効です。 ガベージ コレクターがジェネレーション 0 のコレクションを実行するたびに、ジェネレーション 1 と 2 のオブジェクトを再び調べる必要がなくなります。

    ジェネレーション 0 のコレクションを行うだけでは、アプリケーションが新しいオブジェクトを作成するために必要なメモリを確保できない場合、ガベージ コレクターではジェネレーション 1、ジェネレーション 2 の順にコレクションを実行できます。 ガベージ コレクションでごみだと判断されなかったジェネレーション 1 のオブジェクトは、ジェネレーション 2 に昇格します。

  • ジェネレーション 2: このジェネレーションには、有効期間が長いオブジェクトが格納されます。 有効期間が長いオブジェクトには、たとえば、プロセスの存続期間を通じて有効な静的データを含むサーバー アプリケーションのオブジェクトなどがあります。

    コレクションで解放されなかったジェネレーション 2 のオブジェクトは、その後のコレクションで到達不能であると判断されるまで、ジェネレーション 2 に残ります。

    大きなオブジェクト ヒープ ("ジェネレーション 3" と呼ばれることもあります) 上のオブジェクトは、ジェネレーション 2 でも収集されます。

ガベージ コレクションは、条件に応じて特定のジェネレーションで発生します。 ジェネレーションのコレクションでは、そのジェネレーションとそれよりも前のすべてのジェネレーションのオブジェクトがコレクションの対象になります。 ジェネレーション 2 のガベージ コレクションは、すべてのジェネレーションのオブジェクト (つまり、マネージド ヒープのすべてのオブジェクト) を解放することから、フル ガベージ コレクションとも呼ばれます。

存続と昇格

ガベージ コレクションで解放されなかったオブジェクトは残存オブジェクトと呼ばれ、次のジェネレーションに昇格されます。

  • ジェネレーション 0 のガベージ コレクションで解放されなかったオブジェクトは、ジェネレーション 1 に昇格します。
  • ジェネレーション 1 のガベージ コレクションで解放されなかったオブジェクトは、ジェネレーション 2 に昇格します。
  • ジェネレーション 2 のガベージコレクションで解放されなかったオブジェクトは、ジェネレーション 2 に残ります。

ジェネレーションでごみではないと判断される割合が高いことがガベージ コレクターで検出されると、そのジェネレーションに対する割り当てのしきい値が高くなります。 次のジェネレーションで十分なサイズの解放されたメモリが受け取られます。 CLR では、ガベージ コレクションを遅延させることでアプリケーションのワーキング セットが大きくなりすぎないようにすることと、ガベージ コレクションの実行頻度が多すぎないようにすることに注意して、それらの 2 つの優先事項のバランスを絶えず調整します。

短期のジェネレーションとセグメント

ジェネレーション 0 および 1 のオブジェクトは有効期間が短いことから、それらのジェネレーションのことを "短期ジェネレーション" と呼びます。

短期ジェネレーションは、短期セグメントと呼ばれるメモリ セグメントに割り当てられます。 ガベージ コレクターによって新しいセグメントが取得されると、いずれも新しい短期セグメントになり、ジェネレーション 0 のガベージ コレクションで残ったオブジェクトが格納されます。 古い短期セグメントは新しいジェネレーション 2 のセグメントになります。

短期セグメントのサイズは、システムが 32 ビットと 64 ビットのどちらであるか、および実行されているガベージ コレクターの種類 (ワークステーションの GC またはサーバーの GC) に応じて異なります。 次の表は、短期セグメントの既定のサイズを示しています。

ワークステーションの GC またはサーバーの GC 32 ビット 64 ビット
ワークステーションの GC 16 MB 256 MB
サーバーの GC 64 MB 4 GB
サーバーの GC (論理 CPU が 4 個より多い) 32 MB 2 GB
サーバーの GC (論理 CPU が 8 個より多い) 16 MB 1 GB

短期セグメントには、ジェネレーション 2 のオブジェクトも含めることができます。 ジェネレーション 2 のオブジェクトでは複数のセグメントを、プロセスでの必要に応じてメモリが許容できる限り使用できます。

短期ガベージ コレクションによって解放されるメモリの量は、短期セグメントのサイズまでに限られます。 解放されるメモリの量は、使用されなくなったオブジェクトに占有されていた領域に比例します。

ガベージ コレクションの実行時の動作

ガベージ コレクションには次のフェーズがあります。

  • マーキング フェーズ。有効なすべてのオブジェクトを探し、そのリストを作成します。

  • 再配置フェーズ。圧縮するオブジェクトへの参照を更新します。

  • 圧縮フェーズ。使用されなくなったオブジェクトに占有されている領域を解放し、残ったオブジェクトを圧縮します。 圧縮フェーズでは、ガベージ コレクションで残ったオブジェクトをセグメントの後ろに移動します。

    ジェネレーション 2 のコレクションでは複数のセグメントを占有できるため、ジェネレーション 2 に昇格されたオブジェクトはより古いセグメントに移動できます。 ジェネレーション 1 とジェネレーション 2 の残存オブジェクトは、どちらもジェネレーション 2 に昇格されるため、別のセグメントに移動できます。

    通常、大きなオブジェクト ヒープ (LOH) は圧縮されません。これは、大きなオブジェクトをコピーするとパフォーマンスが低下するためです。 ただし、.NET Core と .NET Framework 4.5.1 以降では、GCSettings.LargeObjectHeapCompactionMode プロパティを使用して、大きなオブジェクト ヒープを必要に応じて圧縮できます。 また、LOH は、次のいずれかを指定することでハード上限が設定されるとき、自動的に圧縮されます。

ガベージ コレクターは、次の情報に基づいてオブジェクトが有効かどうかを判断します。

  • スタック ルート: Just-In-Time (JIT) コンパイラとスタック ウォーカーによって提供されるスタック変数。 JIT の最適化では、スタック変数がガベージ コレクターに報告されるコードの領域が延長または短縮される可能性があります。

  • ガベージ コレクション ハンドル: マネージド オブジェクトを参照するハンドル。これらのハンドルは、ユーザー コードまたは共通言語ランタイムで割り当てることができます。

  • 静的データ: 他のオブジェクトを参照している可能性があるアプリケーション ドメインの静的オブジェクト。 静的オブジェクトはそれぞれのアプリケーション ドメインで追跡されます。

ガベージ コレクションが開始される前に、そのガベージ コレクションをトリガーしたスレッドを除くすべてのマネージド スレッドが中断されます。

次の図は、ガベージ コレクションを発生させて他のスレッドの中断を引き起こすスレッドを示しています。

スレッドでガベージ コレクションをトリガーする方法を示すスクリーンショット。

アンマネージ リソース

アプリケーションで作成されるオブジェクトの大部分については、ガベージ コレクションによって、必要なメモリ管理タスクを自動的に実行できます。 しかし、アンマネージ リソースでは、明示的なクリーンアップが必要です。 最も一般的な種類のアンマネージ リソースは、ファイル ハンドル、ウィンドウ ハンドル、ネットワーク接続などのオペレーティング システム リソースをラップしたオブジェクトです。 ガベージ コレクターは、アンマネージ リソースをカプセル化するマネージド オブジェクトの存続期間を追跡することはできますが、そのリソースのクリーンアップ方法については具体的な情報を持っていません。

アンマネージド リソースをカプセル化するオブジェクトを定義する場合は、そのアンマネージド リソースをクリーンアップするために必要なコードをパブリックな Dispose メソッドという形で提供することをお勧めします。 Dispose メソッドを提供すると、オブジェクトのユーザーがオブジェクトを使い終わったときに、リソースを明示的に解放できます。 アンマネージ リソースをカプセル化するオブジェクトを使用する場合は、必要に応じて Dispose を呼び出すようにしてください。

また、使用する型のコンシューマーが Dispose の呼び出しを忘れた場合に、アンマネージ リソースを解放する手段を用意する必要があります。 セーフ ハンドルを使用してアンマネージ リソースをラップするか、Object.Finalize() メソッドをオーバーライドできます。

アンマネージ リソースのクリーンアップの詳細については、「アンマネージ リソースのクリーンアップ」を参照してください。

関連項目