プログラム実行時、データはメモリ上に格納する必要があります。データが占有する容量と配置場所はデータ型によって決まります。プログラム稼働中はデータ保管用に二つのメモリ領域が使われます:スタック(Stack)、ヒープ(Heap)。
スタック(Stack)
スタックは連続したメモリ配列で、後入れ先出し(LIFO)というデータ構造に従い、主に3種類のデータを保存します。
- 一部の型の変数の値
- プログラムの現在の実行コンテキスト
- メソッドに渡される引数

スタックのメモリ操作はシステムが自動管理するため、開発者が手動で制御する必要はありません。スタックの内部仕組みを理解すると、プログラムの実行フローやC#公式ドキュメントを理解しやすくなります。
スタックの核心的特徴
- データの追加・削除はスタックトップのみで実行可能
- スタックトップにデータを積む動作:Push(プッシュ)
- スタックトップからデータを取り出す動作:Pop(ポップ)
スタックメモリの特徴
- メモリ確保・解放速度が非常に速く、連続したメモリ空間を使用
- ライフサイクルがメソッド呼び出しに紐付き、メソッド実行完了時にスタックフレームが自動破棄
- 保管対象:値型変数、メソッド引数、ローカル変数、実行コンテキスト

ヒープの仕組みを分かりやすくするため、簡単なサンプルコードを確認しましょう
class Program
{
static void Main(string[] args)
{
int a = 10;
int b = 20;
int sum = Add(a, b);
}
static int Add(int x, int y)
{
return x + y;
}
}
Code language: JavaScript (javascript)
このサンプルはとても単純です。プログラムの入口であるMain関数の1行目からint型変数aを作成し、2行目に変数bを定義します。3行目でAdd関数を呼び出しaとbの値を渡し、関数内部へ進みます。xにはaの値、yにはbの値が代入され、関数はx+yの計算結果30を呼び出し元に返しsumに代入します。Addの処理が終わるとx、yがポップされsumの値が30になり、Main関数が完了したらプログラムが終了します。以下でスタック上でこの一連の流れがどう処理されるか段階別に解説します。
プログラム実行フローとスタックのPush/Pop手順





Add関数実行完了後、xとyは関数内のローカル引数なので順番にPopされ、スタックの空き領域を確保しシステムの動作をスムーズに保ちます。
Popされたx、yは消滅し、Addが返す30が呼び出し元へ渡されsumに代入されます。




読者の皆さんは一つ覚えておきましょう。関数実行時の変数作成は上から下へPushされるため、流れはa→b→Add関数に入る→x→yとなります。Addが終わるとx、yがPopされsumがPush、Main終了時には作成順と逆にsum→b→aがPopされ消滅します。
ヒープ(Heap)
ヒープは大きなメモリ領域で、オブジェクト用のメモリブロックを動的に確保する場所です。スタックと異なり、ヒープ内のデータは任意の順番で追加・削除可能です。
プログラム側からヒープ内のオブジェクトを手動で削除することはできません。CLR(共通言語ランタイム)に搭載されたガベージコレクタ GCが、参照先がなくアクセス不能な孤立オブジェクトを自動的に掃除します。これにより他言語で必要な手動メモリ解放に伴うバグを回避できます。
ヒープは広い机のようなもので、自由に物を置けます。一方、先ほど解説したスタックはコインケースのように一端からしか出し入れできません。

GC回収フロー
- プログラムがヒープに3つのオブジェクトを作成し、参照を保持
- プログラムが一つのオブジェクトを使用しなくなり、参照が失われる
- GCが参照のない孤立オブジェクトを検知し、占有メモリを解放
- 回収完了、解放されたメモリは新規オブジェクトに割り当て可能に
ヒープメモリの特徴
- メモリ領域が非連続で、スタックより確保速度が遅い
- ライフサイクルがメソッドに縛られず、GCが一括管理
- 保管対象:参照型インスタンス(クラス、配列、デリゲートなど)。スタックにはオブジェクトのメモリアドレスのみ保存
実例を通して分かりやすく説明します。先述の通りローカルな基本型変数はスタックに保管されますが、ユーザー定義型は基本的にヒープに配置されます。下記コードをご確認ください。
class Program
{
static void Main(string[] args)
{
int a = 10;
int b = 20;
Student tom = new Student("Tom", 12);
Student lucy = new Student("Lucy", 12);
}
}
class Student
{
//メンバー変数
private string name;
private int age;
//コンストラクタ、後ほど解説
public Student(string name, int age)
{
this.name = name;
this.age = age;
}
//メンバーメソッド
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
Code language: JavaScript (javascript)
下図はMain関数内の4つの変数a、b、tom、lucyのメモリ配置図です。スタックの解説と同じく上から下へ実行され、aがPush、次にbがPushされます。aとbの値は直接スタックに保存されます。


例えばtomのデータはヒープの00001領域に存在し、スタックには00001というアドレスが保存されます。このアドレスを通じて後からデータを参照します。
Main関数の実行が完了すると、スタック内のlucy: 01010というアドレス情報がPopされ破棄されます。ここで消えるのはアドレスだけで、ヒープ上の実データは残ったままになります。この時点でヒープのオブジェクトはスタック側からの参照記録をすべて失います。


メモリ領域について
スタックとヒープはソフトウェア層で定義されたメモリ区分で、物理ハードウェアの区切りではありません。
スタック・ヒープはソフト側の論理的なメモリ分割に過ぎず、物理RAMハードウェアに「スタック領域/ヒープ領域」と明示された専用モジュールは存在しません。
物理RAMメモリは単一のメモリチップで連続した物理アドレスを持つだけで、スタックとヒープを区別する標識は一切ありません
OSは実行中の各プロセスに独立した仮想メモリ空間を割り当て、この仮想メモリを事前に4つの論理セグメントに分割します。
コードセグメント:コンパイル後のプログラム命令を保管
グローバル静的セグメント:静的変数、定数を保管
スタックセグメント:プログラムのメインスレッド・サブスレッドがそれぞれ独立したスタックメモリを保有
ヒープセグメント:動的メモリ確保に使用する巨大な共有メモリプール
ここで言うスタック・ヒープはOSがプロセスごとに割り当てた論理領域に過ぎず、ハードウェアレベルでの隔離は存在しません。
スタックとヒープの違い