LASSIC Media らしくメディア

2026.10.06 らしくコラム

スタックとヒープの違い、メモリ領域の使い分けと落とし穴

監修・編集責任者:牛尾 昭昌(株式会社LASSIC 執行役員)

灰色の台の上に置かれたメモリモジュールの基板と、縁に並ぶ金色の端子を斜め上から接写したパソコン部品の写真。

この記事の結論

  • スタックは関数の呼び出しに合わせて自動で積み下ろしする領域、ヒープは実行中に大きさを指定して確保し、解放か回収で空きに戻す領域です。
  • 大きさが実行時に決まる値や関数から戻った後も使う値はヒープへ、関数の中で使い終わる小さな値はスタックへ置くのが基本です。
  • つまずきやすいのは深い再帰によるスタックの使い過ぎとスレッド数の見積もりで、上限の設定値を文書に残して監視します。

※ 本記事は2026年10月時点の公式情報(省庁・公的機関、および製品やサービスを提供する事業者が公開している資料)に基づきます。

スタックとヒープの違いは、実行中のプログラムが使うメモリ領域の、確保と解放の決まり方の違いです。スタックは関数を呼ぶたびに積まれ、戻ると片づく領域、ヒープは必要なときに大きさを指定して確保し、使い終わったら解放する(または自動で回収される)領域を指します。関数の中で作った文字列を返したら中身が壊れていた、再帰を深くしたらプロセスが落ちた、スレッドを増やしたらメモリが足りなくなった——。こうした不具合をたどると、この違いに行き着くことがよくあります。

なお、データ構造にも「スタック」(後から入れたものを先に取り出す入れ物)と「ヒープ」(優先度付きキューの実装に使う木構造)があります。名前は同じでも、ここで扱うメモリ領域とは別の話です。データ構造のスタックは「スタックとキューの違い」で説明しています。本記事では、C言語の規格やJava仮想マシン仕様、各言語とLinuxの公式ドキュメントをもとに、仕組み、コード例、使い分け、つまずきやすい点を整理します。

スタックとヒープとは

Rust公式の解説書『The Rust Programming Language』は、スタックもヒープも実行時にコードが使えるメモリの一部で、構造が違うと説明しています。スタックは値を受け取った順に積み、逆の順に取り除く後入れ先出し(LIFO)の領域で、置ける値は大きさがコンパイル時に決まっていて変わらないものに限られます。大きさが分からない、あるいは変わりうるデータはヒープに置きます。

ヒープに置くときは、必要な大きさを指定して場所を求めます。メモリアロケーター(確保を受け持つ仕組み)が十分に大きい空きを探して使用中の印を付け、その場所のアドレスであるポインターを返します。ポインター自体は大きさが決まっているのでスタックに置けますが、中身を読むにはポインターをたどる必要があります。*1

おもしろいことに、C言語の規格(ISO/IEC 9899。ここではC11の最終草案N1570を参照)には「stack」も「heap」も一度も出てきません。規格が定めるのは記憶域期間(オブジェクトの寿命を決める区分)で、静的・スレッド・自動・割り付けの4種類があります。関数の中で宣言した普通の変数は自動記憶域期間で、そのブロックに入ってから抜けるまで生きます。mallocなどで確保した割り付け記憶域期間のオブジェクトは、確保から解放まで生きます。*2 スタックとヒープは、この寿命の違いを実現するための仕組みだと考えると整理しやすくなります。

確保と解放の仕組み

スタックとヒープの違いを示した図。左のスタックはスレッドごとに1つあり、下からmain、f、gの順に関数のフレームが積まれ、呼ぶと上に積み、戻ると取り除く。fのフレームにあるポインターpが、右のヒープの確保済みの領域を指す。ヒープはスレッド間で共有され、確保済みの領域と空きが混在し、空きを探して確保し、解放か回収で空きに戻る。スタックには大きさが決まっていて関数の中で使い終わる値、ヒープには大きさが実行時に決まる値や関数から戻った後も使う値を置く。

関数を呼ぶと、渡された引数(ヒープ上のデータを指すポインターを含む)と関数のローカル変数がスタックに積まれ、関数が終わると取り除かれます。呼び出しが入れ子になるほど上へ積み重なり、戻るたびに上から片づいていきます。置く場所は常に一番上なので、探す手間がかかりません。

ヒープでは、アロケーターが十分な大きさの空きを探したうえで、次の確保に備えた管理の手間もかかります。Rustの解説書は、このためスタックへ積むほうがヒープへの確保より速く、ポインターをたどる分だけヒープのデータへのアクセスも一般に遅いと説明しています。*1 ただし差の大きさは処理系や使い方で変わるため、速さを理由に書き方を選ぶなら計測してから決めます。

スレッドとの関係も押さえておきます。Java仮想マシン仕様では、スレッドごとに専用のJVMスタックがスレッドと同時に作られ、ヒープはすべてのスレッドで共有されます。スタック上の値はそのスレッドだけのもの、ヒープ上のオブジェクトは複数のスレッドから参照されうるものです。プロセスとスレッドでメモリの共有範囲がどう違うかは「プロセスとスレッドの違い」で扱っています。

スタックとヒープの違い

スタックとヒープの違い(Rust公式の解説書・JVM仕様・C言語の規格をもとにこの記事で整理)
観点 スタック ヒープ
確保と解放 関数の呼び出しと戻りに合わせて自動 必要なときに確保し、明示的に解放するか、ガベージコレクターが回収
置ける値 大きさがコンパイル時に決まる値 大きさが実行時に決まる値、変わりうる値
寿命 関数やブロックを抜けるまで 解放されるまで、または参照されなくなって回収されるまで
スレッドとの関係 スレッドごとに1つ スレッド間で共有
足りなくなったとき スタックオーバーフロー(JavaはStackOverflowError、LinuxはSIGSEGV) 確保の失敗(Cはヌルポインター、JavaはOutOfMemoryError)

違いの根っこは寿命の決まり方です。スタックの値は関数から戻ると寿命が終わり、ヒープの値は、プログラムが解放を指示するか、参照が無くなって処理系が回収するまで残ります。

なお、両者は物理的に別の部品ではありません。JVM仕様は、JVMスタックはフレームの積み下ろし以外に直接操作されないため、フレーム自体をヒープに確保してもよく、メモリが連続している必要もないとしています。*3 同じメモリを違う規則で管理する領域だと捉えると、言語ごとの違いも理解しやすくなります。

言語ごとの扱い方

C言語では、ヒープの確保(malloc)と解放(free)をプログラマーが書きます。確保できなければヌルポインターが返り、解放済みの領域や、確保の関数が返していないポインターをfreeに渡したときの動作は未定義です。*2

Javaでは、クラスのインスタンスと配列はすべてヒープに確保されます。オブジェクトが明示的に解放されることはなく、ガベージコレクターが回収します。仕様は回収の方式を特定しておらず、実装者が選べます。*3 回収の方式の違いは「ガベージコレクションとは」で説明しています。

Rustは、所有権の仕組みでヒープを管理します。解説書は、ヒープ上のどのデータをどのコードが使っているかを追い、重複を減らし、使われなくなったデータを片づけることが、所有権が解決する問題だと述べています。

Goでは、どちらに置くかをコンパイラーが決めます。公式FAQは、正しさの面では知る必要はないとしたうえで、関数から戻った後に参照されないことを証明できない変数は、ガベージコレクションの対象となるヒープに置くと説明しています。この判定がエスケープ解析です。*4

具体例:コードで見る置き場所

C言語で、関数の中の配列を指すポインターを返すと何が起きるかを見てみます。

#include <stdlib.h>
#include <string.h>

char *bad_copy(const char *s) {
    char buf[32];                 /* 自動記憶域期間:関数を抜けると寿命が終わる */
    strncpy(buf, s, sizeof buf - 1);
    buf[sizeof buf - 1] = 0;
    return buf;                   /* 寿命の切れる領域を指すポインターを返してしまう */
}

char *good_copy(const char *s) {
    char *p = malloc(strlen(s) + 1);  /* 割り付け記憶域期間:free まで生きる */
    if (p == NULL) return NULL;       /* 確保できなければヌルポインター */
    strcpy(p, s);
    return p;                         /* 呼び出し側が free する */
}

bad_copyのbufは、関数を抜けた時点で寿命が終わります。規格は、寿命の外でオブジェクトを参照したときの動作を未定義とし、それを指していたポインターの値も不定になるとしています。*2 たまたま正しい文字列が読めることもあるため、テストをすり抜けやすい不具合です。good_copyはヒープに確保するので関数を抜けても残りますが、代わりに呼び出し側がfreeしなければなりません。

Goでは、同じ形の書き方をしても壊れません。

package main

type point struct{ x, y int }

func newPoint() *point {
	p := point{1, 2} // 戻った後も使われるので、コンパイラーがヒープに置く
	return &p
}

func sum() int {
	q := point{3, 4} // 関数の中で使い終わるので、スタックに置ける
	return q.x + q.y
}

func main() { println(newPoint().x, sum()) }

FAQによれば、アドレスを取った変数はヒープに置く候補になりますが、エスケープ解析で関数から戻った後に使われないと分かれば、スタックに残せます。*4 go build -gcflags=-m でビルドするとコンパイラーの最適化の判断が表示され、-mを重ねるほど詳しくなります。*5 どの変数がヒープに移されたかを、推測ではなく出力で確かめられます。

実務での使い分け

日々の開発でスタックとヒープを意識する場面は、主に3つあります。

  • 大きさが実行時に決まるデータや、関数から戻った後も使うデータはヒープに置く。大きなローカル変数も、スタックよりヒープのほうが向くことがある
  • 関数の中で使い終わる小さな値はスタックに置く。JavaやGoでは処理系が決めるので、計測で確保が多いと分かった箇所だけ書き方を見直す
  • スレッドを大量に作る設計では、1本あたりのスタックの大きさにスレッド数を掛けてメモリを見積もる

3つ目は見落とされがちです。LinuxのNPTLでは、プログラム開始時のRLIMIT_STACKのソフト制限が無制限以外なら、その値が新しいスレッドの既定のスタックの大きさになります。無制限の場合は、多くのアーキテクチャーで2MBが使われます。*6 JavaのスレッドスタックはLinux/x64の既定で1024KBで、-Xssで変えられます。*7 数千のスレッドを立てる設計なら、ヒープとは別にこの分のメモリを見込んでおきます。

一方、Goのゴルーチンは数キロバイトの小さなスタックで始まり、足りなくなると実行時に伸び、不要になれば縮みます。OSのスレッドとは、スタックの見積もり方が変わります。

つまずきやすい点

最も多いのはスタックの使い過ぎです。setrlimitのマニュアルによれば、RLIMIT_STACKはプロセスのスタックの大きさの上限で、そこに達するとSIGSEGVが発生します。*8 Javaでは、スレッドの計算が許されたJVMスタックより大きくなるとStackOverflowErrorが投げられます。終わりの条件を誤った再帰や、深い木構造の走査のように深さが入力データで決まる再帰が典型です。エラー時の出力の読み方は「スタックトレースの読み方」にまとめています。

上限を上げるのは応急処置にとどめ、再帰をループに書き換える、深さに上限を設ける、大きな配列をヒープへ移す、といった直し方を先に検討します。いまの上限はシェルのulimit -sで確かめられます。Goには1本のゴルーチンのスタックの上限を決めるruntime/debugのSetMaxStackがあり、初期値は64ビット環境で1GBです。無限の再帰による被害を抑えるのが主な用途とされています。*9

ヒープ側で注意したいのは、JavaのOutOfMemoryErrorです。仕様上、ヒープが足りないときだけでなく、新しいスレッドのJVMスタックを作るメモリが足りないときにも、同じOutOfMemoryErrorが投げられます。*3 エラー名だけで「ヒープを増やせばよい」と決めず、メッセージとスレッド数も確かめます。ヒープの上限は-Xmxで指定し、公式マニュアルは、サーバー用途では-Xmsと同じ値にすることが多いと説明しています。

Goでは、仮想メモリの使用量が大きく見えることもあります。FAQによれば、アロケーターが確保用に大きな仮想メモリの領域を予約するためで、実際に使っている量はtopコマンドのRES(Linux)の列で確かめます。解放漏れを疑うときの調べ方は「外部エンジニアのメモリリーク調査」を参照してください。

外部に委託するときに確認しておきたい点

開発や性能改善を外部に頼むときは、まずメモリの前提を設定値として文書に残すかを確かめます。スレッドの数、1本あたりのスタックの大きさ、ヒープの上限(Javaなら-Xmx)、コンテナに割り当てるメモリの上限は互いに関係するため、どれか一つだけを変えると別の箇所で不足が起こります。

次に、再帰の深さが入力データで決まる箇所を洗い出してもらいます。上限を超えたときにプロセスごと落ちるのか、エラーを返して処理を続けるのかは、業務への影響を左右する設計上の判断です。

三つ目は、速さを理由にした書き方の根拠です。「スタックに置いたほうが速い」という説明だけで大きな書き換えを進めず、プロファイラーの結果やGoの-gcflags=-mの出力など、計測で示してもらいます。

最後に運用です。本番でのヒープとスレッド数の監視、StackOverflowErrorやOutOfMemoryErrorの検知と通知、障害時にヒープダンプを取る手順まで、提案の段階で確かめておきます。

まとめ:スタックとヒープで確かめておきたい3つの点

スタックとヒープの違いを実務に生かすうえで、確かめておきたい点は3つに整理できます。第一に、違いの根っこは寿命の決まり方にあり、関数から戻れば消える値はスタック、戻った後も使う値や大きさが実行時に決まる値はヒープに置くこと。第二に、JavaやGoのように処理系が置き場所を決める言語でも、スタックの上限とスレッド数、ヒープの上限は設計の段階で見積もり、設定値として残しておくこと。第三に、深い再帰によるスタックの使い過ぎやOutOfMemoryErrorには、上限を上げる前に計測と監視で原因を確かめることです。

LASSICに相談するメリット

LASSICは、元請(プライムベンダー)としてシステムの開発と保守・運用を受託しています。メモリが関わる不具合や性能改善では、JavaならJDK Flight Recorderとヒープダンプ、Goならpprofと-gcflags=-mの出力、C/C++ならAddressSanitizerを使い、計測で原因を確かめます。設計では、スレッドとゴルーチンの使い分け、スタックの大きさとスレッド数、-Xmxとコンテナのメモリ上限の関係、再帰の深さの上限を決めます。運用では、GitHub ActionsでAddressSanitizerを有効にしたテストと負荷試験を回し、Terraformで実行環境の設定を管理し、PrometheusとGrafanaでヒープとスレッド数を監視します。

よくある質問

データ構造のスタックやヒープと関係はありますか

メモリのスタックは、後から積んだものを先に取り除く点でデータ構造のスタックと同じ考え方です。一方、メモリのヒープは、優先度付きキューに使う木構造のヒープとは名前が同じだけの別物です。

スタックに置いたほうが常に速いのですか

Rust公式の解説書は、スタックへ積むほうがヒープへの確保より速く、ヒープのデータへのアクセスも一般に遅いと説明しています。ただし差は処理系や使い方で変わるため、書き方を変える前に計測で確かめるのがおすすめです。

ガベージコレクションがある言語でも意識する必要はありますか

Go公式のFAQは、正しさの面では置き場所を知る必要はないが、効率には影響すると説明しています。深い再帰によるスタックの使い過ぎは、ガベージコレクションの有無にかかわらず起こります。

スタックの上限はどうやって確かめますか

Linuxでは、シェルでulimit -sを実行するといまの上限が分かります。Javaのスレッドは-Xss、C言語でpthreadを使う場合はpthread_attr_setstacksizeで、スレッドごとの大きさを指定できます。

メモリ設計と性能改善のご相談

元請(プライムベンダー)として、メモリの使い方の見直しから性能の検証、保守・運用までご提案します。

Remoguとリラシクなら、性能改善やアプリケーションの保守・運用に加わるITエンジニアも探せます。

Remoguは、リモート前提で全国から即戦力のITプロ人材を調達するサービスです。リラシクは、扱う求人がすべてリモートワークのITエンジニア専門転職エージェントです。どちらもLASSICが運営しています。

無料相談はこちら

出典

  1. *1 参考:The Rust Project「The Rust Programming Language」第4.1章 What Is Ownership?(https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html)。出典:節「The Stack and the Heap」のうち、LIFO・固定の大きさ・アロケーターとポインター・速さの違い・関数呼び出しとスタック・所有権が解決する問題の記述を参照(2026年10月確認)
  2. *2 参考:ISO/IEC JTC1/SC22/WG14「N1570 Committee Draft」(ISO/IEC 9899:201x、2011年4月12日)(https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf)。出典:6.2.4(記憶域期間の4種類・寿命・自動記憶域期間・寿命の外での参照)、7.22.3(メモリ管理関数・確保失敗時のヌルポインター)、7.22.3.3(free)を参照(2026年10月確認)
  3. *3 参考:Oracle「The Java Virtual Machine Specification, Java SE 21 Edition」第2章(https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-2.html)。出典:2.5.2(Java Virtual Machine Stacks・StackOverflowError・OutOfMemoryError)、2.5.3(Heap・ガベージコレクター)を参照(2026年10月確認)
  4. *4 参考:The Go Authors「Frequently Asked Questions (FAQ)」(https://go.dev/doc/faq)。出典:「How do I know whether a variable is allocated on the heap or the stack?」「Why does my Go process use so much virtual memory?」「Why goroutines instead of threads?」を参照(2026年10月確認)
  5. *5 参考:The Go Authors「cmd/compile」(https://pkg.go.dev/cmd/compile)。出典:コマンドラインのフラグのうち、-m(最適化の判断を表示)の説明を参照(2026年10月確認)
  6. *6 参考:Linux man-pages「pthread_create(3)」(https://man7.org/linux/man-pages/man3/pthread_create.3.html)。出典:NOTESのうち、NPTLでの既定のスタックの大きさとRLIMIT_STACK、無制限時の既定値(多くのアーキテクチャーで2MB)の記述を参照(2026年10月確認)
  7. *7 参考:Oracle「The java Command」(Java SE 21)(https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html)。出典:-Xss(スレッドスタックの大きさ・Linux/x64の既定値1024KB)、-Xmx(ヒープの上限)の説明を参照(2026年10月確認)
  8. *8 参考:Linux man-pages「getrlimit(2)/setrlimit(2)」(https://man7.org/linux/man-pages/man2/setrlimit.2.html)。出典:RLIMIT_STACK(プロセスのスタックの大きさの上限・到達時のSIGSEGV)の記述を参照(2026年10月確認)
  9. *9 参考:The Go Authors「runtime/debug」(https://pkg.go.dev/runtime/debug)。出典:func SetMaxStack(ゴルーチンのスタックの上限・64ビット環境での初期値1GB)の説明を参照(2026年10月確認)




View