C#や.NETでの開発において、メモリ管理を意識することはC++などに比べて少ないかもしれません。
それは、「ガベージコレクション(Garbage Collection / GC)」という強力な機能が、不要になったメモリを自動的に解放してくれるからです。
しかし、アプリケーションのパフォーマンスを追求したり、長時間稼働するシステムを開発したりするようになると、このGCの挙動が非常に重要になってきます。
「解放されるはずのメモリが減らない(メモリリーク)」
こういった問題は、GCの仕組みを正しく理解していないと解決できません。
この記事では、C#におけるガベージコレクションの基本的な仕組み(世代別GC)から、実行されるタイミング、開発者が意図的に実行する方法(GC.Collect)、そしてGCだけに頼ってはいけない「アンマネージド・リソース」の管理まで、サンプルコード付きで徹底解説します。
![]() 執筆者:マヒロ |
|
- OS:Windows 11 / macOS Sequoia
- IDE:Visual Studio / VS Code / IntelliJ IDEA
- その他:Chrome DevTools / 各言語最新安定版
※本メディアでは、上記環境にてコードの動作と情報の正確性を検証済みです。
ガベージコレクション(GC)とは?基本的な役割
ガベージコレクションとは、プログラムが使用したメモリ領域(ヒープ領域)のうち、「もう使われなくなったデータ(ゴミ)」を自動的に検出して解放する機能のことです。
C言語やC++では、プログラマが手動で malloc して free するといったメモリ管理を行う必要がありましたが、C#ではCLR(Common Language Runtime)がこれを代行してくれます。
これにより、メモリの解放忘れによるバグやクラッシュを大幅に減らすことができます。
どのデータが「ゴミ」とみなされるのか?
GCは、アプリケーション内のどの変数からも「参照されなくなったオブジェクト」を不要なもの(ゴミ)と判断します。
ルート(実行中のメソッドのローカル変数や、静的変数など)から参照を辿っていき、どこからも到達できないオブジェクトが回収の対象となります。
「世代別」ガベージコレクションの仕組み
.NETのガベージコレクタは、効率的にメモリを管理するために「世代(Generation)」という概念を取り入れています。
これにより、すべてのメモリを毎回チェックするのではなく、効率よくゴミ掃除を行っています。
3つの世代(ジェネレーション)
オブジェクトは、作られてからの期間によって以下の3つの世代に分類されます。
- ジェネレーション0(Gen 0)
- 最も新しく作られたオブジェクトがここに属します。
- 多くのオブジェクト(一時変数など)は短命であるため、GCは頻繁にこのGen 0を掃除します。これを「エフェメラルGC」と呼び、非常に高速に終わります。
- ジェネレーション1(Gen 1)
- Gen 0の掃除を生き残ったオブジェクトが昇格します。
- Gen 0とGen 2の間のバッファのような役割を果たします。
- ジェネレーション2(Gen 2)
- Gen 1の掃除を生き残った、長寿命のオブジェクトがここに属します。
- 静的変数やキャッシュデータなどが該当します。Gen 2の掃除(フルGC)はコストが高く、アプリケーションの停止時間も長くなるため、頻発させないことがパフォーマンスチューニングの鍵となります。
GCが実行されるタイミングはいつ?
開発者がGCを意識しなくても、以下の条件が満たされた時に自動的に実行されます。
- メモリ不足を検知した時:システムがメモリ不足(Low Memory)を通知してきた場合、GCが積極的に動いて空き容量を確保しようとします。
- 割り当てられたメモリ使用量(閾値)を超えた時:各世代ごとに設定されたメモリ使用量の閾値を超えたタイミングで、その世代(およびそれより若い世代)のGCが走ります。
- 手動で呼び出された時:プログラムコードから
GC.Collect()メソッドを呼び出すことで、強制的に実行させることができます。
基本的には、CLRが最適なタイミングを判断して実行するため、開発者が介入する必要はありません。
ガベージコレクションを強制的に実行する方法(手動実行)
デバッグ目的や、特定の大量データ処理が終わった直後など、明示的にメモリを掃除したい場合には GC.Collect() メソッドを使用します。
ただし、基本的には使用を避けるべきです。
理由は後述します。
GC.Collectメソッドの使い方
using System;
class Program
{
static void Main()
{
// 大量のデータを生成してメモリを消費させる
CreateGarbage();
// メモリ使用量を表示
long memoryBefore = GC.GetTotalMemory(false);
Console.WriteLine($"GC実行前のメモリ: {memoryBefore} bytes");
// ガベージコレクションを強制実行
// すべての世代を対象にし、完了するまで待機する
GC.Collect();
GC.WaitForPendingFinalizers(); // ファイナライザの完了待ち
GC.Collect(); // ファイナライズされたオブジェクトを回収するために再度実行
// GC後のメモリ使用量を表示
long memoryAfter = GC.GetTotalMemory(false);
Console.WriteLine($"GC実行後のメモリ: {memoryAfter} bytes");
}
static void CreateGarbage()
{
// 1万個の配列を作成(メソッドを抜けるとゴミになる)
for (int i = 0; i < 10000; i++)
{
var temp = new int[100];
}
}
}
実行結果
GC実行前のメモリ: 42056 bytes
GC実行後のメモリ: 3120 bytes
GC.Collect() を呼び出すことで、強制的にガベージコレクションを実行しています。
CreateGarbage メソッド内で生成された大量の配列は、メソッド終了時点で参照が切れているため、GCによって回収され、メモリ使用量が減っていることが確認できます。
なお、GC.WaitForPendingFinalizers() は、デストラクタ(ファイナライザ)を持つオブジェクトの処理が終わるのを待つメソッドです。
完全にメモリをクリーンにしたい場合、この待機処理を挟んで2回 Collect を呼ぶパターンが使われることがあります。
なぜ手動実行は推奨されないのか?
GC.Collect() を頻繁に呼ぶと、かえってパフォーマンスが悪化する可能性があります。
無理やりGCを走らせると、まだ回収しなくていいオブジェクトまでチェックすることになり、CPUリソースを無駄に消費します
また、本来ならGen 0で回収されるはずだったオブジェクトが、無理なGCによって生き残り、Gen 1やGen 2に昇格してしまうことで、メモリに長く居座る原因にもなります。
「メモリが解放されないから GC.Collect を書く」というのは対症療法であり、まずはメモリリークの原因を探るのが先決です。
メモリが解放されない原因と「GC対象外」のリソース
「GCがあるのにメモリ不足になる」という場合、GCが回収できない状態(参照が残っている)か、そもそもGCの管理外のリソースを使っている可能性があります。
1. static変数の参照しっぱなし(メモリリーク)
static(静的)なリストやディクショナリにデータを追加し続けると、アプリケーションが終了するまで参照が残り続けるため、GCの回収対象になりません。
これが典型的なメモリリークです。
不要になったデータは Remove や Clear で削除する必要があります。
2. イベントハンドラの解除忘れ
イベントにメソッドを登録(+=)すると、イベント発生元がそのオブジェクトへの参照を保持します。
登録したオブジェクトが不要になっても、イベントの解除(-=)を忘れると参照が残り続け、GCによって回収されなくなります。
3. アンマネージド・リソース(GC対象外)
ファイルハンドル、データベース接続、ネットワークソケット、画像ビットマップなどのOSレベルのリソース(アンマネージド・リソース)は、.NETのGCだけでは完全に管理できません。
これらは開発者が責任を持って解放処理を行う必要があります。
アンマネージド・リソースの解放には「Dispose」と「using」を使う
GCが自動で片付けてくれないリソースを使う場合は、IDisposable インターフェースを実装したクラスを使用し、使い終わったら Dispose() メソッドを呼ぶのがC#のルールです。
usingステートメントによる自動解放
手動で Dispose() を呼ぶと、例外が発生した際に呼ばれないリスクがあります
そのため、using ステートメントを使って自動的に解放されるように記述します。
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "test.txt";
string content = "このテキストはファイルに書き込まれます。";
// usingステートメントを使う(C# 8.0以降の書き方)
// ブロックを抜けると自動的に Dispose() が呼ばれ、ファイルが閉じられる
using (StreamWriter writer = new StreamWriter(filePath))
{
writer.WriteLine(content);
Console.WriteLine("書き込み完了");
}
// ここではもう writer オブジェクトは破棄されている(リソース解放済み)
}
}
StreamWriter はファイル操作を行うクラスで、IDisposable を実装しています。
using (...) ブロックで囲むことで、書き込み処理が終わった後(またはエラーで中断した後)に、自動的かつ確実に Dispose() メソッドが呼び出されます。
これにより、ファイルが開いたままロックされるといったトラブルを防ぐことができます。
GCは「メモリ」を掃除してくれますが、「ファイルロック」などは解除してくれません。
だからこそ Dispose が必要なのです。
C#のスキルを活かして年収を上げる方法
以上、C#のガベージコレクション(GC)の仕組みとタイミングなどについて解説してきました。
業務システム開発やアプリ開発、ゲーム開発において需要の高いC#を扱えるエンジニアは、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。
なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。
転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。
| 年収アップにこだわりたい方 (平均アップ額138万円の実績) | テックゴー |
| 未経験・経験者問わず幅広く探したい方 | ユニゾンキャリア |
| 業界に精通した担当者に相談したい方 | キッカケエージェント |
| ゲーム業界への転職を志望する方 | ファミキャリ |
| エンジニア未経験からキャリアを築く方 | イーチキャリア |



