スタックとヒープの違い

プログラム実行時、データはメモリ上に格納する必要があります。データが占有する容量と配置場所はデータ型によって決まります。プログラム稼働中はデータ保管用に二つのメモリ領域が使われます:スタック(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手順

変数aを作成する際、組み込み基本型は直接スタックに確保されPush処理が実行されます
2行目のコードを実行し、値20のint型変数bを作成します。先に積まれたデータはスタック下部、後から積まれたデータはスタックトップに配置されます
3行目のコードでは、イコール右辺のAdd(a,b)が先に実行され関数呼び出しが行われます。aの値がxに、bの値がyにコピーされます。これは値渡しのため、aとxは完全に独立した別の変数です。
この段階ではx+yの計算だけを行い結果を返すため、スタックに永続的に保存する必要はありません。計算結果30が呼び出し元に渡されint sum = 30となるイメージです。return x+yが実行されAdd関数が終了すると、関数内のローカル変数はすべてPop処理され、変数が占有していたメモリ領域が消去され再利用可能になります。
スタックの一番上の位置をスタックトップ(Top)と呼び、Pop後はトップの位置が下に移動します。
Add関数実行完了後、xとyは関数内のローカル引数なので順番にPopされ、スタックの空き領域を確保しシステムの動作をスムーズに保ちます。
Popされたx、yは消滅し、Addが返す30が呼び出し元へ渡されsumに代入されます。
int sum = 30を実行すると再度Push処理が走り、値30のsumがスタックトップに積まれます。この文が完了するとMain関数は終了準備に入り、sum、b、aといったMain配下のローカル変数が順次Popで破棄されます。
Popは必ずスタック上部から実行されます。スタックは出入り口が一つしかない通路のため、積む時も取り出す時も同じ端を使用します。そのためMain内の変数破棄順は作成時と逆になり、sum→b→aの順で消去されます。
変数bがPopされ破棄され、スタックトップの位置が自動的に下に移動します。
すべての変数がPopされ、最後のaも消去されました。厳密に言うとMain関数にはもう一つローカル変数が存在します。Main(string[] args)の文字列配列argsで、コンソール起動時に渡されるコマンドライン引数を受け取る役割です。この配列はメモリ上に二箇所に分かれて保管され、後述するヒープと関連する内容のため、今回の解説では割愛します。

読者の皆さんは一つ覚えておきましょう。関数実行時の変数作成は上から下へPushされるため、流れはa→b→Add関数に入る→x→yとなります。Addが終わるとx、yがPopされsumがPush、Main終了時には作成順と逆にsum→b→aがPopされ消滅します。


ヒープ(Heap)

ヒープは大きなメモリ領域で、オブジェクト用のメモリブロックを動的に確保する場所です。スタックと異なり、ヒープ内のデータは任意の順番で追加・削除可能です。

プログラム側からヒープ内のオブジェクトを手動で削除することはできません。CLR(共通言語ランタイム)に搭載されたガベージコレクタ GCが、参照先がなくアクセス不能な孤立オブジェクトを自動的に掃除します。これにより他言語で必要な手動メモリ解放に伴うバグを回避できます。

ヒープは広い机のようなもので、自由に物を置けます。一方、先ほど解説したスタックはコインケースのように一端からしか出し入れできません。

ヒープは広い机と同じで、自由にデータを配置できます。開発者がヒープオブジェクトを手動で消す必要はなくGC(ガベージコレクタ)が管理します。オブジェクトを参照する箇所がなくなると、GCが不定期にオブジェクトを破棄しメモリを回収します。これにより開発者の負担が大幅に減り、メモリ解放漏れによるプログラムクラッシュを防げます。C++などの言語では開発者が手動でヒープメモリを解放する必要があります。

GC回収フロー

  1. プログラムがヒープに3つのオブジェクトを作成し、参照を保持
  2. プログラムが一つのオブジェクトを使用しなくなり、参照が失われる
  3. GCが参照のない孤立オブジェクトを検知し、占有メモリを解放
  4. 回収完了、解放されたメモリは新規オブジェクトに割り当て可能に

ヒープメモリの特徴

  • メモリ領域が非連続で、スタックより確保速度が遅い
  • ライフサイクルがメソッドに縛られず、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の値は直接スタックに保存されます。

基本型変数は直接スタック上に保管されます
自作のStudent型の実データはヒープに保管され、スタックにはヒープ上のメモリアドレスだけが記録されます
例えばtomのデータはヒープの00001領域に存在し、スタックには00001というアドレスが保存されます。このアドレスを通じて後からデータを参照します。

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

ヒープ内の01010領域にあるLucyオブジェクトは、スタック上にアドレスを記録する変数がなくなり参照切れの孤立オブジェクトとなります。ガベージコレクタGCは適切なタイミングでこのオブジェクトを破棄します。
Lucyオブジェクトはスタックの参照アドレスがPopされたため、どこからも参照されない状態になります。GCは任意のタイミングでこのオブジェクトのメモリを消去し、領域を再利用可能にします。オブジェクトを参照する箇所が一つでも残っている限り、GCは消去しません。

メモリ領域について

スタックとヒープはソフトウェア層で定義されたメモリ区分で、物理ハードウェアの区切りではありません。

スタック・ヒープはソフト側の論理的なメモリ分割に過ぎず、物理RAMハードウェアに「スタック領域/ヒープ領域」と明示された専用モジュールは存在しません。

物理RAMメモリは単一のメモリチップで連続した物理アドレスを持つだけで、スタックとヒープを区別する標識は一切ありません

OSは実行中の各プロセスに独立した仮想メモリ空間を割り当て、この仮想メモリを事前に4つの論理セグメントに分割します。

コードセグメント:コンパイル後のプログラム命令を保管

グローバル静的セグメント:静的変数、定数を保管

スタックセグメント:プログラムのメインスレッド・サブスレッドがそれぞれ独立したスタックメモリを保有

ヒープセグメント:動的メモリ確保に使用する巨大な共有メモリプール

ここで言うスタック・ヒープはOSがプロセスごとに割り当てた論理領域に過ぎず、ハードウェアレベルでの隔離は存在しません。

スタックとヒープの違い

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です