.NET Framework
ガベージコレクション
サーチ…
前書き
.Netではnew()で作成されたオブジェクトはマネージヒープ上に割り当てられます。これらのオブジェクトは、それらを使用するプログラムによって明示的にファイナライズされることはありません。代わりに、このプロセスは.Netガベージコレクタによって制御されます。
以下の例は、ガーベッジ・コレクターが動作していることを示す「ラボ・ケース」とその動作の重要な一部を示していますが、ガーベッジ・コレクターによる適切な処理のためのクラスの準備方法に焦点を当てています。
備考
ガベージコレクタは、割り当てられたメモリの観点からプログラムのコストを下げることを目的としていますが、そのためには処理時間の点でコストがかかります。全体的な妥協点を達成するために、ガベージコレクタを使用してプログラミングする際に考慮する必要があるいくつかの最適化があります。
- Collect()メソッドを明示的に呼び出す場合は、メモリが実際に必要な場合にのみデッドオブジェクトを終了させる「最適化」モードの使用を検討してください
- Collect()メソッドを呼び出す代わりに、実際に必要な場合にのみメモリコレクションを起動するAddMemoryPressure()メソッドとRemoveMemoryPressure()メソッドの使用を検討してください
- メモリー・コレクションは、すべてのデッド・オブジェクトのファイナライズを保証するものではありません。代わりに、ガベージコレクタは3つの「世代」を管理します。オブジェクトは世代から次の世代に「生き残る」ことがあります
- ガベージコレクタスレッドと他のアプリケーションスレッドとの間の干渉の度合いが異なる結果となる、セットアップの微調整を含む様々な要因に応じて、いくつかのスレッドモデルが適用される可能性があります。
(ゴミ)収集の基本例
次のクラスを考えます:
public class FinalizableObject
{
public FinalizableObject()
{
Console.WriteLine("Instance initialized");
}
~FinalizableObject()
{
Console.WriteLine("Instance finalized");
}
}
それを使わなくても、インスタンスを作成するプログラム:
new FinalizableObject(); // Object instantiated, ready to be used
次の出力を生成します。
<namespace>.FinalizableObject initialized
何も起こらなければ、プログラムが終了するまでオブジェクトはファイナライズされません(管理されたヒープ上のすべてのオブジェクトを解放し、プロセス中でこれらを終了させます)。
次のように、ガベージコレクタを強制的に特定のポイントで実行することができます。
new FinalizableObject(); // Object instantiated, ready to be used
GC.Collect();
次の結果が得られます。
<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized
今回は、ガベージコレクタが呼び出されるとすぐに、未使用(「死んでいる」)オブジェクトが確定され、管理されたヒープから解放されました。
ライブオブジェクトとデッドオブジェクト - 基本
大まかなルール:ガベージコレクションが発生した場合、「ライブオブジェクト」はまだ使用されているオブジェクトであり、「デッドオブジェクト」は使用されなくなったものです(コレクションが発生する前にスコープを参照している変数またはフィールドがあれば、 。
次の例(便宜上、FinalizableObject1とFinalizableObject2は上記の例のFinalizableObjectのサブクラスであるため、初期化/終了メッセージの動作を継承しています)。
var obj1 = new FinalizableObject1(); // Finalizable1 instance allocated here
var obj2 = new FinalizableObject2(); // Finalizable2 instance allocated here
obj1 = null; // No more references to the Finalizable1 instance
GC.Collect();
出力は次のようになります。
<namespace>.FinalizableObject1 initialized
<namespace>.FinalizableObject2 initialized
<namespace>.FinalizableObject1 finalized
ガベージコレクタが呼び出された時点で、FinalizableObject1は死んでいるオブジェクトであり、FinalizableObject2はライブオブジェクトであり、マネージヒープ上に保持されます。
複数の死んだオブジェクト
2つ(または複数)の死んだオブジェクトが互いに参照している場合はどうなりますか?これは、OtherObjectがFinalizableObjectのパブリックプロパティであると仮定して、以下の例に示されています。
var obj1 = new FinalizableObject1();
var obj2 = new FinalizableObject2();
obj1.OtherObject = obj2;
obj2.OtherObject = obj1;
obj1 = null; // Program no longer references Finalizable1 instance
obj2 = null; // Program no longer references Finalizable2 instance
// But the two objects still reference each other
GC.Collect();
これにより、次の出力が生成されます。
<namespace>.FinalizedObject1 initialized
<namespace>.FinalizedObject2 initialized
<namespace>.FinalizedObject1 finalized
<namespace>.FinalizedObject2 finalized
2つのオブジェクトは、参照されているにもかかわらず、管理されたヒープからファイナライズされ、解放されます(実際に存在するオブジェクトのいずれにも他の参照が存在しないため)。
弱い参照
弱参照とは、他のオブジェクト(「ターゲット」とも呼ばれます)への参照ですが、それらのオブジェクトがガベージコレクションされないようにするための「弱い」ものです。言い換えれば、ガベージコレクタがオブジェクトを「生きている」または「死んだ」と評価した場合、弱い参照はカウントされません。
次のコード:
var weak = new WeakReference<FinalizableObject>(new FinalizableObject());
GC.Collect();
出力を生成します。
<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized
オブジェクトは、WeakReference変数によって参照されているにもかかわらず、管理されたヒープから解放されます(ガベージコレクタが呼び出された時点ではまだスコープ内にあります)。
結果#1:WeakReferenceターゲットがまだ管理されているヒープに割り当てられているかどうかを判断することは、いつでも安全ではありません。
結果#2:プログラムがWeakreferenceのターゲットにアクセスする必要がある場合は、ターゲットがまだ割り当てられているかどうかの両方の場合にコードを提供する必要があります。ターゲットにアクセスする方法はTryGetTargetです:
var target = new object(); // Any object will do as target
var weak = new WeakReference<object>(target); // Create weak reference
target = null; // Drop strong reference to the target
// ... Many things may happen in-between
// Check whether the target is still available
if(weak.TryGetTarget(out target))
{
// Use re-initialized target variable
// To do whatever the target is needed for
}
else
{
// Do something when there is no more target object
// The target variable value should not be used here
}
WeakReferenceの汎用バージョンは.Net 4.5以降で利用可能です。すべてのフレームワークのバージョンは、同じ方法でビルドされ、次のようにチェックされる非ジェネリックの型なしのバージョンを提供します。
var target = new object(); // Any object will do as target
var weak = new WeakReference(target); // Create weak reference
target = null; // Drop strong reference to the target
// ... Many things may happen in-between
// Check whether the target is still available
if (weak.IsAlive)
{
target = weak.Target;
// Use re-initialized target variable
// To do whatever the target is needed for
}
else
{
// Do something when there is no more target object
// The target variable value should not be used here
}
Dispose()とファイナライザ
オブジェクトがもはや使用されなくなるとすぐに解放されるメモリを確保する手段として、Dispose()メソッドを実装し(包含するクラスをIDisposableとして宣言する) 「キャッチ」とは、Dispose()メソッドが呼び出されるという強い保証がないということです(オブジェクトの終わりに常に呼び出されるファイナライザとは異なります)。
1つのシナリオは、明示的に作成したオブジェクトに対してDispose()を呼び出すプログラムです。
private void SomeFunction()
{
// Initialize an object that uses heavy external resources
var disposableObject = new ClassThatImplementsIDisposable();
// ... Use that object
// Dispose as soon as no longer used
disposableObject.Dispose();
// ... Do other stuff
// The disposableObject variable gets out of scope here
// The object will be finalized later on (no guarantee when)
// But it no longer holds to the heavy external resource after it was disposed
}
別のシナリオでは、フレームワークによってクラスがインスタンス化されると宣言しています。この場合、新しいクラスは通常、基本クラスを継承します。たとえば、MVCでは、System.Web.Mvc.ControllerBaseのサブクラスとしてコントローラクラスを作成します。基本クラスがIDisposableインターフェイスを実装する場合、これはフレームワークによってDispose()が適切に呼び出されることを示すヒントですが、ここでも強い保証はありません。
したがって、Dispose()はファイナライザの代わりではありません。代わりに、2つは異なる目的で使用する必要があります。
- ファイナライザは最終的にリソースを解放し、そうでなければ発生するメモリリークを回避します
- Dispose()は、リソースが不要になったらすぐにリソースを解放し、メモリ割り当て全体の負担を軽減します。
オブジェクトの適切な処分とファイナライズ
Dispose()とファイナライザは異なる目的を目的としているため、外部のメモリが重いリソースを管理するクラスで両方を実装する必要があります。その結果、次の2つのシナリオが適切に処理されるようにクラスを作成します。
- ファイナライザのみが呼び出されたとき
- 最初にDispose()が呼び出され、後でファイナライザが呼び出されると
1つの解決策は、クリーンアップコードを1回または2回実行すると、1回だけ実行するのと同じ結果が得られるような方法でクリーンアップコードを書き込むことです。実行可能性は、クリーンアップの性質によって異なります。たとえば、次のようになります。
- すでに閉じられているデータベース接続を閉じると効果がないため動作します
- いくつかの "使用回数"を更新するのは危険で、1度ではなく2度呼び出すと間違った結果になります。
より安全な解決策は、設計によって、クリーンアップコードが一度だけ呼び出され、外部コンテキストが何であれ一度だけ呼び出されることです。これは、専用のフラグを使用して "古典的な方法"を達成することができます:
public class DisposableFinalizable1: IDisposable
{
private bool disposed = false;
~DisposableFinalizable1() { Cleanup(); }
public void Dispose() { Cleanup(); }
private void Cleanup()
{
if(!disposed)
{
// Actual code to release resources gets here, then
disposed = true;
}
}
}
代わりに、ガベージコレクタは、Disposeが呼び出された後にファイナライザをスキップできるようにする、特定のメソッドSuppressFinalize()を提供します。
public class DisposableFinalizable2 : IDisposable
{
~DisposableFinalizable2() { Cleanup(); }
public void Dispose()
{
Cleanup();
GC.SuppressFinalize(this);
}
private void Cleanup()
{
// Actual code to release resources gets here
}
}