.Net Framework 4.5.2のガベージ・コレクタの挙動を調べるために、C#のコードで実験してみた記事です。
※ 本記事のサンプルコードは、川俣晶氏の以下記事を参考にさせていただきました。
ローカル変数の場合
以下コードでは、Queue<string[]>
型のオブジェクトをローカル変数として宣言します。キューに巨大データを詰め込んで拡張した後、GC.Collect()
でマネージドヒープのゴミ回収をしますが、GC.Collect()
前後でのメモリ使用量(近似値)を出力しています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GCTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
var q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
static Queue<string[]> CreateQueue()
{
var q = new Queue<string[]>();
string[] a = new string[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = new String('A', 1000);
}
q.Enqueue(a);
string[] b = new string[1000];
for (int i = 0; i < 1000; i++)
{
b[i] = new String('A', 1000);
}
q.Enqueue(b);
string[] c = new string[1000];
for (int i = 0; i < 1000; i++)
{
c[i] = new String('A', 1000);
}
q.Enqueue(c);
string[] d = new string[1000];
for (int i = 0; i < 1000; i++)
{
d[i] = new String('A', 1000);
}
q.Enqueue(d);
return q;
}
}
}
リリースモードで実行すると、結果は以下となりました。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :40124
GC.Collect()
によって7.7MBのメモリが解放されたのは分かるのですが、GC.Collect()
の呼出し時点ではq
はスコープを抜けてないのだから、メモリ解放されるのおかしくね?と思いました。
ですが、これってGC.Collect()
からq
のスコープ末尾「}
」までの間に、q
が使用されていないから解放されたのかなあと思います。
試しに、q
を使用するコードを追加してみました。
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
var q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
q.Count(); // qを参照
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
q
の使用を追加した上記コードをリリースモードで実行してみると、ライフタイムが延長されたのか、メモリ解放されませんでした。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :8120136
ちなみにデバッグモードで実行すると、最適化されないせいか、q
を使用しなくてもライフタイムが延長せず、メモリも解放されていませんね。
The highest generation is 2.
1. Before CreateQueue() :31772
2. After CreateQueue() :8113124
3. After GC.Collect() :8111892
GC.KeepAlive()
GC.KeepAlive()
は、オブジェクトのライフタイムを延長します。このメソッドは「この時点までライフタイムを延長します」とマーカするものです。
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
var q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
GC.KeepAlive(q); // この時点までqのライフタイムを延長
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
ライフタイムが延長され、予想通りガベコレの回収対象になりませんでした。
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue() :8219792
3. After GC.Collect() :8224688
オブジェクトのライフタイムを制限
ブロックで閉じてオブジェクトのライフタイムをexpiredさせれば、デバッグモードでもガベコレに回収されんじゃね?と思い、以下のコードを書いてみました。オブジェクトq
を{}
内に閉じ込めています。
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
{
var q = CreateQueue();
}
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
あれ、解放されませんね…。デバッグモードってそういうものなんですね。
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue() :8123664
3. After GC.Collect() :8112208
ただし、GC.Collect()
の前にq
にnull
を代入すると、デバッグモードでもq
が使用するリソースは解放されました。
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
{
var q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
q = null;
}
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue() :8123664
3. After GC.Collect() :32100
static変数の場合
q
をstatic変数にしてみました。
static Queue<string[]> q;
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
するとどうでしょう。q
はガベコレに回収されませんでした。まあ、static
なのだからそりゃそうですね。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :8120136
static変数の場合(nullを代入)
q
がガベコレの対象になるように、ガベコレ直前にq = null;
を追加してみました。
static Queue<string[]> q;
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
q = CreateQueue();
q = null; // ガベコレの対象になるはず
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
おお、ガベコレに回収されてる。やったね。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :40124
static変数の場合(Dequeue()でキューを空にする)
次はnull
を代入する代わりに、キューの全要素をDequeue()
で空にしてみます。
static Queue<string[]> q;
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
var slotCount = q.Count;
for (int i = 0; i < slotCount; ++i)
q.Dequeue();
Console.WriteLine($"3. After Dequeue() :{GC.GetTotalMemory(false)}");
GC.Collect();
Console.WriteLine($"4. After GC.Collect() :{GC.GetTotalMemory(false)}");
q = null;
GC.Collect();
Console.WriteLine($"5. After set null to q :{GC.GetTotalMemory(false)}");
}
解放されますね。要素数が大きいときは、Queue.TrimExcess()
で空きスロットをshrinkすれば、よりオーバーヘッドが減ると思います。
The maximum generation is 2.
1. Before CreateQueue() :64584
2. After CreateQueue() :8219792
3. After Dequeue() :8219792
4. After GC.Collect() :65048
5. After set null to q :64728
static変数の場合(Clear()でキューを空にする)
static Queue<string[]> q;
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
q.Clear();
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
Clear()
でも同じく、ガベコレに回収されるですね。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :40184
static変数の場合(new演算子で領域を再割り当て)
q.Clear();
の代わりに、q = new Queue<string[]>();
に変更したところ、やはりガベコレに回収されました。
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue() :8113144
3. After GC.Collect() :40156
new
による再割当て前の領域はダングリング参照とはならずに、ちゃんとガベコレに回収されるんですね(それがガベコレの仕事なのだが…)。
static変数の場合(クラスのstaicメンバ)
キュー(static領域)をメンバにもつクラスのインスタンス変数にnull
を代入したケースです。
class QueueTest
{
static public Queue<string[]> q;
}
namespace GCTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
GC.Collect();
Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
var aQueueTest = new QueueTest();
QueueTest.q = CreateQueue();
Console.WriteLine($"2. After CreateQueue() :{GC.GetTotalMemory(false)}");
aQueueTest = null;
GC.Collect();
Console.WriteLine($"3. After GC.Collect() :{GC.GetTotalMemory(false)}");
}
// ...
インスタンスにnull
を代入しても、メンバが参照するリソースはstatic
領域にあるため、リソースはガベコレに回収されませんね。
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue() :8219792
3. After GC.Collect() :8224688
staticメンバにnullを代入してstatic
領域のリソースへの参照を断つと、やっとガベコレに回収されました。
// aQueueTest = null;
QueueTest.q = null;
GC.Collect();
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue() :8219792
3. After GC.Collect() :64752
おわりに
くぅ疲です。「ガベージ・コレクタ」というと、高度なアルゴリズムで武装された、頭の良い人だけが意識する近代兵器というイメージがあったのですが、こうやって実コードで動作を確認してみると、意外と身近なものに思えてくるものですね。筆者はガベコレのことが、ちょっと好きになりました。