メモリ管理(スタックとヒープ)
プログラムが使うメモリは「スタック」と「ヒープ」に大きく分かれる。自動で片付く速い領域と、自分で確保して管理する自由な領域。その違いを押さえると、メモリリークやスタックオーバーフローの正体が見えてくる。
- 1.プロセスのメモリは コード/データ/ヒープ/スタック の4領域。可変なのはヒープとスタック。
- 2.スタックは 自動・LIFO・高速:関数呼び出しごとに積み下げされ、関数を抜けると自動で解放。
- 3.ヒープは 動的確保・手動 or GC:寿命を自分で管理する代わりに自由。放置するとメモリリークになる。
プロセスのメモリ構成
OS はプロセスを起動するとき、そのプロセス専用のメモリ空間(アドレス空間)を割り当てます。中身はおおまかに4つの領域に分かれます。
高位アドレス
┌──────────────┐
│ スタック │ ← 関数呼び出し・ローカル変数(下に伸びる ↓)
├──────────────┤
│ ↓ │
│ (空き) │
│ ↑ │
├──────────────┤
│ ヒープ │ ← 動的確保したデータ(上に伸びる ↑)
├──────────────┤
│ データ領域 │ ← グローバル変数・静的変数(.data / .bss)
├──────────────┤
│ コード領域 │ ← 機械語の命令(.text、通常は読み取り専用)
└──────────────┘
低位アドレス
- コード領域(text):プログラムの機械語命令。実行中に書き換わらないので読み取り専用。
- データ領域(data / bss):グローバル変数や静的変数。プロセスの寿命のあいだずっと存在する。
- ヒープ:実行中に動的に確保するメモリ。伸び縮みする。
- スタック:関数呼び出しとローカル変数のための領域。これも伸び縮みする。
サイズが固定されないのは ヒープとスタック の2つです。図のように両者は空き領域を挟んで向かい合わせに配置され、片方が伸びすぎてもう片方とぶつかると破綻します(後述)。
データ構造としての スタック(LIFO のコレクション) と、ここで言う コールスタック(関数呼び出しのためのメモリ領域) は別物です。ただしコールスタックが LIFO で動くのは、まさにスタックというデータ構造そのものだから。名前が同じなのは偶然ではありません。
スタック:自動・LIFO・高速
スタックは 関数呼び出し のための領域です。関数を呼ぶたびに スタックフレーム(活動レコード) が1つ積まれ、そこに次のものが入ります。
- 関数の ローカル変数
- 関数の 引数
- 戻り先アドレス(呼び出し元に帰る場所)
そして関数を抜けると、そのフレームは まるごと捨てられます。最後に積んだものが最初に外れる LIFO(Last In, First Out) なので、解放はスタックポインタを動かすだけ。だから 非常に高速 で、プログラマが解放を意識する必要がありません。
function a() が b() を呼び、b() が c() を呼ぶと…
呼び出し前 a 実行中 b 実行中 c 実行中 c 終了後
┌─────┐
┌─────┐ │ c │ ┌─────┐
┌─────┐ │ b │ ├─────┤ │ b │
(空) │ a │ │ a │ │ a │ │ a │
└─────┘ └─────┘ └─────┘ └─────┘
その代わり、スタックに置けるのは基本的に コンパイル時にサイズが決まるデータ や、その関数の中だけで使う一時的なデータ です。寿命は「関数の実行中」に縛られます。
ヒープ:動的確保・手動 or GC
ヒープは 実行時に、必要なサイズを必要なときに確保する ための領域です。スタックと違い、確保したメモリは関数を抜けても生き続けます。だからこそ「関数をまたいで共有するデータ」「実行するまでサイズが分からないデータ」に向きます。
代わりに、いつ解放するかを誰かが管理しなければなりません。方式は大きく2つです。
- 手動管理:C/C++ など。
malloc/free、new/deleteで自分で確保・解放する。自由だが、解放忘れ=メモリリーク、二重解放=バグの温床。 - 自動管理(GC):Java・C#・Go・JavaScript・Python など。ガベージコレクタ が「もう誰からも参照されていないオブジェクト」を見つけて自動回収する。楽だが、回収のタイミングや一時停止(GC ポーズ)は制御しにくい。
// C:手動管理の例
int *p = malloc(sizeof(int) * 100); // ヒープに確保
// ... p を使う ...
free(p); // 自分で解放する。忘れるとリーク
p = NULL; // 解放後に触らないよう無効化
「GC 言語ならメモリリークは無縁」は誤解です。GC は “到達不能”になったものだけ を回収します。グローバルな配列やキャッシュ、解除し忘れたイベントリスナなどから参照が残り続けると、不要なオブジェクトでも 回収されず溜まり続けます。これも立派なメモリリークです。
スタック vs ヒープ
| 観点 | スタック | ヒープ |
|---|---|---|
| 確保のされ方 | 関数呼び出しで自動 | 実行時に明示的に要求 |
| 解放 | 関数を抜けると自動 | 手動 or GC が回収 |
| 寿命 | その関数の実行中だけ | 解放/回収されるまで(関数をまたげる) |
| 速度 | 非常に速い(ポインタ移動のみ) | 比較的遅い(空き管理が必要) |
| サイズ | 小さめ・上限あり | 大きく確保できる |
| 主な失敗 | スタックオーバーフロー | メモリリーク・断片化 |
「ローカル変数=スタック、new したもの=ヒープ」が基本ですが、絶対ではありません。Java では基本型のローカル変数はスタック、オブジェクト本体はヒープ(変数は参照を持つ)。Go ではコンパイラの エスケープ解析 が「関数の外へ漏れるか」を判断し、漏れる値だけヒープに回します。JavaScript はプリミティブ以外を実質ヒープで扱います。配置は言語・処理系が決めるもの、と覚えておくと混乱しません。
つまずきポイント①:スタックオーバーフロー
スタックには 上限サイズ があります(OS やスレッド設定で決まり、数 MB 程度が一般的)。フレームを積みすぎてこの上限を超えると、スタックオーバーフロー でプログラムが落ちます。
典型的な原因は 終了条件のない(または深すぎる)再帰 です。
factorial(n) を「n == 0 で止める」条件なしに書くと…
factorial(3)
└ factorial(2)
└ factorial(1)
└ factorial(0)
└ factorial(-1)
└ factorial(-2) ← 止まらずフレームが積み続ける → 💥 オーバーフロー
巨大な配列をローカル変数(スタック上)に取る、再帰が深くなりすぎる、といったケースも該当します。深い再帰が必要なら ループに書き換える、あるいは ヒープに状態を持つ ことで回避できます。
無限ループは CPU を食い続けるだけで(普通は)落ちませんが、無限再帰はスタックを食いつぶして即クラッシュします。「再帰には必ず終了条件(ベースケース)を置く」——これが鉄則です。
つまずきポイント②:メモリリーク
メモリリーク とは、もう使わないのに解放されず、使用メモリが増え続ける 状態です。ヒープで起きます。短時間では気づきにくく、長時間動くサーバやアプリで少しずつメモリを圧迫し、最終的に性能劣化やクラッシュ(OS による強制終了=OOM Kill)を招きます。
よくある原因:
- 手動管理:
free/deleteの呼び忘れ、エラー時に解放処理を通らないパス。 - GC 管理:グローバル変数・静的コレクションへの溜め込み、解除し忘れたコールバック/リスナ、意図せず残る クロージャ の参照。
ヒープを長く使うと、確保と解放を繰り返すうちに空き領域が細切れになる 断片化(フラグメンテーション) が起こり、「合計の空きは足りるのに大きな塊が取れない」状態になることがあります。リークとは別物ですが、これも長寿命プロセスがメモリで苦しむ原因の一つです。
まとめ
- メモリは コード/データ/ヒープ/スタック。可変なのは ヒープとスタック。
- スタック:関数の出入りで自動・LIFO・高速。寿命は関数の中だけ。積みすぎると スタックオーバーフロー。
- ヒープ:動的確保で自由・長寿命。手動 or GC で管理し、放置すると メモリリーク。
- どちらに置くかは 言語・処理系が決める。仕組みを知っておくと、落ち方やメモリ肥大の原因を切り分けられます。
メモリ空間そのものがどう作られ、物理メモリと結び付くのかは 仮想メモリ を、実行単位とメモリ共有の関係は プロセスとスレッド を合わせて読むと、全体像がつながります。
OS Article
メモリ管理(スタックとヒープ)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
メモリ管理
比較で見る軸
難易度: intermediate / カテゴリ: OS / タグ数: 4
導入後に効く点
スタックは 自動・LIFO・高速:関数呼び出しごとに積み下げされ、関数を抜けると自動で解放。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- OS
- タグ数
- 4
判断チェックリスト
- 自社の用途が「メモリ管理 / スタック」に近いか確認する。
- 強みである「プロセスのメモリは コード/データ/ヒープ/スタック の4領域。可変なのはヒープとスタック。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。