C#のメモリ管理入門|スタック・ヒープ・値型・参照型を初心者向けに解説
C#のプログラムで、変数やオブジェクトがメモリ上でどのように扱われるのかを考えてみましょう。
最初に重要なのは、「値型は必ずスタック、参照型は必ずヒープ」と単純には決められないことです。この記事では、初学者向けの基本モデルを説明したうえで、実際の.NETでは異なる場合がある部分も区別します。
この記事で分かること
- スタックとマネージドヒープの役割
- 値型と参照型の本当の違い
- クラス、構造体、列挙型がどのように保持されるか
- staticフィールドとインスタンスフィールドの違い
- ガベージコレクション(GC)がオブジェクトを回収する仕組み
学習用の4領域
この記事では、基本情報技術者試験や初学者向け授業で理解しやすいように、メモリの役割を「プログラム領域」「静的領域」「ヒープ領域」「スタック領域」の4つに分けて説明します。
これは学習用に単純化したモデルであり、.NETランタイム内部の厳密なメモリ区分とは異なります。実際の.NETでは、実行コード、型情報、staticデータなどが、より複雑な方法で管理されます。
| 学習用の領域 | 主に入るもの | 寿命の目安 |
|---|---|---|
| プログラム領域(コード領域) | メソッドの実行用命令 | 通常はプログラムの実行中 |
| 静的領域 | 型ごとに共有されるstaticフィールドの値 | 通常はプログラムの実行中 |
| マネージドヒープ | クラスのインスタンス、配列、文字列などの参照型の実体 | 到達不能になった後、GCが回収 |
| スタック | メソッド呼び出しに必要な情報、引数、ローカル変数など | 対応するメソッドから戻るときに解放 |
よくある読み違い
- staticのないクラスがプログラム領域、staticのあるクラスが静的領域、という分け方ではありません。
- インスタンスメソッドもstaticメソッドも、命令としてのコードはプログラム領域として考えます。
- staticフィールドの値は、学習用モデルでは静的領域として考えます。
- 「newを使ったものはすべてヒープ」と判断することもできません。値型に対しても
newを使えるためです。
プログラム領域(コード領域)
学習用モデルでは、プログラムを実行するための命令が格納される領域です。インスタンスメソッドもstaticメソッドも、実行する命令としてのコードはこちらにあると考えます。
クラスは、データと処理をまとめた型の定義です。インスタンスメンバーを使うには通常newでインスタンスを作ります。一方、staticメンバーはインスタンスを作らず、クラス名から利用できます。
静的領域
学習用モデルでは、staticフィールドの値を保持する領域です。staticフィールドはインスタンスごとのデータではなく、型ごとに1組だけ存在し、すべてのインスタンスから共有されます。
staticメソッドのコードをここへ置くと考えるのではありません。静的領域として捉えるのは、staticフィールドが保持している値です。
通常のアプリケーションでは、staticフィールドの値はプログラムが動いている間保持されます。プログラムを終了すると値は失われ、次回の起動時には初期値から始まります。次回起動時にも値を残したい場合は、ファイルやデータベースなどへ保存する必要があります。
マネージドヒープ
クラスのインスタンス、配列、文字列など、参照型の実体が割り当てられる領域です。
メソッドが終了しても、どこかから参照されているオブジェクトは残ります。どこからも到達できなくなるとGCの回収対象になり、将来のガベージコレクションでメモリが再利用できる状態になります。参照されなくなった瞬間に必ず回収されるわけではありません。
Dispose()は、ファイルやネットワーク接続などのリソースを解放するための仕組みです。オブジェクトのメモリをその場でマネージドヒープから消す命令ではありません。
スタック
スタックは、メソッド呼び出しに使われる作業用の領域です。呼び出し元へ戻るための情報、引数、ローカル変数などを含むスタックフレームが積み重なります。
LIFO(Last In First Out:後入れ先出し)で管理され、メソッドから戻ると対応するスタックフレームが解放されます。スタックフレームの確保と解放は規則的で高速ですが、通常のC#コードでは、プログラマーが任意の変数の配置場所を直接選ぶわけではありません。
次のコードは、参照型を理解するための学習用モデルです。
void Demo()
{
// playerはPlayerオブジェクトへの参照を保持します。
// new Player()で作られた実体はマネージドヒープに置かれます。
var player = new Player();
}
// Demo終了後、ほかに参照がなければ
// PlayerオブジェクトはGCの回収対象になります。
説明では参照を「住所」にたとえることがありますが、C#の参照を、プログラムから直接操作できる生のメモリアドレスと考えるのは正確ではありません。また、ローカル変数が実際にスタック、レジスタなどのどこへ配置されるかは、JITコンパイラの最適化によって変わる場合があります。
値型と参照型の違い
値型と参照型の本質的な違いは、スタックかヒープかではなく、変数が値そのものを保持するか、オブジェクトへの参照を保持するかです。
| 値型 | 参照型 | |
|---|---|---|
| 主な例 | int、bool、struct、enum | class、配列、string、delegate |
| 変数が保持するもの | 値そのもの | オブジェクトへの参照 |
| 代入 | 値がコピーされる | 参照がコピーされる |
| 配置場所 | 使用方法やランタイムの最適化による | 実体は通常マネージドヒープ |
コピーの違いを詳しく確認したい場合は、関連記事「参照型と値型をテレビのたとえで理解する」も参考にしてください。ただし、配置場所は「値型だから必ずスタック」とは限らない点に注意してください。
クラスと構造体の配置
クラス
クラスは参照型です。new Player()などで作成したクラスのインスタンスは、通常マネージドヒープに割り当てられます。変数は、そのオブジェクトへの参照を保持します。
オブジェクトへの参照がどこからも到達できなくなると、オブジェクトはGCの回収対象になります。メソッドが終了した瞬間やDispose()を呼び出した瞬間に、必ずメモリから消えるわけではありません。
構造体
構造体は値型です。構造体の変数は値そのものを保持し、別の変数へ代入すると通常は値全体がコピーされます。
構造体の実際の配置場所は、使用方法と.NETランタイムの最適化によって決まります。
- ローカル変数:初学者向けモデルでは、スタックフレーム内に保持されると考えられます。ただし、JITの最適化によりレジスタへ保持されるなど、必ずスタックに置かれるとは限りません。
- クラスのインスタンスフィールド:通常、ヒープ上にあるクラスオブジェクトの内部へ値が直接含まれます。構造体だけが別のヒープオブジェクトになるわけではありません。
- 構造体配列:各要素の値が通常、配列オブジェクトの内部に直接並びます。
List<T>:構造体の値が内部配列に保持されます。通常、ジェネリックなList<T>へ格納するだけではボックス化されません。- ボックス化:値型を
object型や対応するインターフェイス型として扱うと、値をコピーしたオブジェクトがマネージドヒープに作られます。
構造体とクラスの使い分けは「C#構造体とクラスの違いと使い分け」、ボックス化については「C#のボクシングとアンボクシング」も参考になります。
列挙型(enum)
C#の列挙型(enum)は値型の一種で、関連する定数へ名前を付けて表現する型です。構造体と同じく値型に分類されますが、C#の型分類では構造体型とは別の列挙型です。
基になる整数型(基礎型)は、指定しなければintです。必要に応じてbyte、sbyte、short、ushort、int、uint、long、ulongを指定できます。
enum PlayerState : byte
{
Waiting,
Playing,
GameOver
}
列挙型の値も、構造体と同様に使用場所によって配置が変わります。object型などへ変換してボックス化すると、値を含むオブジェクトがマネージドヒープに作られます。
staticフィールドとインスタンスフィールド
class Player
{
// 型全体で1つだけ共有される
public static int PlayerCount = 0;
// Playerインスタンスごとに存在する
public int Score = 0;
public Player()
{
PlayerCount++;
}
}
var player1 = new Player();
var player2 = new Player();
player1.Score = 10;
player2.Score = 20;
Console.WriteLine(Player.PlayerCount); // 2
Console.WriteLine(player1.Score); // 10
Console.WriteLine(player2.Score); // 20
PlayerCountは型ごとに1つだけ存在します。一方、Scoreはplayer1とplayer2が別々に持っています。staticの有無でクラス全体の置き場所が変わるわけではありません。
publicやprivateなどのアクセス修飾子も通常どおり有効です。staticは「誰からでも使える」という意味ではなく、「インスタンスではなく型に属し、型ごとに1組だけ存在する」という意味です。
staticフィールドが初期化されるタイミング
staticフィールドは型ごとに1組だけ存在し、型が利用される過程で初期化されます。厳密な初期化タイミングは、明示的なstaticコンストラクターがあるかどうかなどによって異なります。
そのため、「必ず最初のインスタンスを作った瞬間に初期化される」とは限りません。初心者の段階では、型ごとに共有され、通常はプログラムの実行中保持されると捉えてください。
まとめ
- 4領域は理解を助けるための学習モデルであり、.NET内部の厳密な区分ではありません。
- 値型と参照型の違いは、スタック/ヒープではなく、値そのものを保持するか参照を保持するかです。
- 参照型の実体は通常マネージドヒープに置かれ、到達不能になるとGCの回収対象になります。
- 構造体やenumは値型ですが、必ずスタックに置かれるとは限りません。
- staticフィールドは型ごとに1組だけ存在します。プログラムを再起動すると、保存していない値は引き継がれません。
参考資料
- Microsoft Learn:自動メモリ管理
- Microsoft Learn:値型
- Microsoft Learn:参照型
- Microsoft Learn:列挙型
- Microsoft Learn:staticコンストラクター
- .NET Framework Internals: How the CLR Creates Runtime Objects(Web Archive)
改訂版PDF資料
この記事の内容に合わせて、授業・投影用のPDF資料を32ページに整理しました。スタックとマネージドヒープ、値型と参照型、GC、static、構造体、enum、List<T>のCountとCapacityを、図とコードで確認できます。
- 値型と参照型の違いを、配置場所ではなく「値そのもの/参照を保持する」という観点で整理
- staticメソッドの命令と、staticフィールドが保持する値を区別
nullの代入、到達不能、GCによる回収のタイミングを区別- 構造体の配置は、使用方法やJITの最適化によって変わることを明記
List<T>の要素数(Count)と内部配列の容量(Capacity)を区別
図は理解を助けるための学習用モデルです。.NET内部の厳密な物理配置を示すものではありません。実際の配置や寿命は、使用方法、JITの最適化、実行環境によって変わる場合があります。
改訂版PDF「C#で正しく学ぶ メモリ管理(2026年改訂版)」を別画面で開く









ディスカッション
コメント一覧
まだ、コメントがありません