---
title: "Rustのスタックとヒープ｜値がヒープへ脱出する条件とスタックオーバーフローの防ぎ方"
url: "https://www.issoh.co.jp/tech/details/2814/"
published: 2024-06-27
updated: 2026-10-18
categories: ["Rust"]
publisher: "株式会社一創"
---

# Rustのスタックとヒープ｜値がヒープへ脱出する条件とスタックオーバーフローの防ぎ方

Rustの値は、何もしなければスタックに置かれます。ヒープへ移るのは、`Box`・`Vec`・`String`のようにヒープを確保する型を自分で選んだときだけです。Goのエスケープ解析のように、コンパイラが使われ方を見て値を勝手にヒープへ逃がすことはありません。では、どんな値をヒープへ**脱出**させるべきか。答えは「サイズがコンパイル時に決まらない」「型が自分自身を含む」「大きすぎてスタックに置くと困る」の3条件です。本記事では、この3条件と所有権による解放のしくみを整理したうえで、[Rust 1.99.1](https://blog.rust-lang.org/2026/10/15/Rust-1.99.1/)（2026年10月15日公開）で実行した結果をもとに、`Box::new([0u8; N])`がデバッグビルドで落ちる理由、スレッドのスタックサイズの変え方、メモリ使用量の減らし方までを扱います。

## まとめ：Rustのスタック・ヒープと脱出条件の要点

- **既定はスタック**：整数・`bool`・固定長配列・構造体など、サイズがコンパイル時に決まる値はスタックに置かれる。ヒープを使うのは`Box`・`Vec`・`String`などを選んだときだけ。
- **ヒープへ脱出させる条件は3つ**：①`dyn Trait`やスライスなどサイズが決まらない型を値として持つ、②連結リストのように自分自身を含む型を定義する、③数MB単位の大きなデータを扱う。
- **解放は所有権が決める**：所有者がスコープを抜けると`drop`されヒープが返る。ガベージコレクタは無い。`Rc`の循環参照だけは解放されないので、片側を`Weak`にする。
- **大きな配列は`Box::new`でなく`vec!`で作る**：16MiBの配列を`Box::new([0u8; N])`で作ると、デバッグビルドではスタックオーバーフローで終了した（終了コード134）。`vec![0u8; N].into_boxed_slice()`や`Box::new_zeroed()`は通った。
- **スタックサイズはスレッド単位で変える**：生成したスレッドの既定は2MiB。`thread::Builder::stack_size`か環境変数`RUST_MIN_STACK`で広げる。メインスレッドのサイズはRustではなくOSが決める。
- **メモリ使用量はenumのサイズで大きく変わる**：1KiBのバリアントを持つenumを100万個並べると約1,025MB、そのバリアントを`Box`で包むと約19MBになった。

## Rustのメモリ配置｜スタック・ヒープ・静的領域の3つ

実行中のRustプログラムが値を置く場所は、スタック・ヒープ・静的領域の3つです。どこに置かれるかは型で決まり、`malloc`や`new`を直接書く場面はふつうありません。

| 領域   | 置かれるもの                  | 確保と解放             | 大きさの上限              |
| ---- | ----------------------- | ----------------- | ------------------- |
| スタック | ローカル変数・引数（固定サイズ）        | 関数の呼び出しと復帰で自動     | スレッドごとに固定（既定2MiBなど） |
| ヒープ  | `Box`・`Vec`・`String`の中身 | 所有者のスコープ終了で`drop` | OSが許す限り             |
| 静的領域 | `static`・文字列リテラル        | プログラムの終了まで残る      | コンパイル時に確定（実行時に増えない） |

### スタック：関数フレームと固定サイズの値

スタックは後入れ先出しで、関数を呼ぶたびに引数とローカル変数をまとめた**スタックフレーム**が積まれ、関数を抜けると捨てられます。確保も解放もスタックポインタを動かすだけなので速い反面、容量はスレッドごとに固定です。[The Rust Programming Language 日本語版の「所有権とは？」](https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html)が書くとおり、スタックに置けるのは既知の固定サイズのデータに限られます。

```
fn main() {
    let n = 10;            // i32：スタック
    let sum = add(n, 20);  // add のフレームが積まれ、戻ると捨てられる
    println!("{sum}");
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}
```

### ヒープ：伸び縮みするデータの本体

ヒープは、実行時までサイズが決まらないデータや、あとから伸び縮みするデータの置き場です。`Vec`や`String`は「ポインタ・長さ・容量」の3つをスタックに置き、**データ本体だけをヒープに確保**します。確保のたびにアロケータが空き領域を探し、読むときもポインタをたどる必要があるため、スタックより遅くなります（[TRPL「所有権とは？」](https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html)の説明）。

### 静的領域：staticと文字列リテラル

`static`変数と文字列リテラル（`&'static str`）はバイナリに埋め込まれ、プログラムの終了まで残ります。書き換えたい共有状態を`static mut`で持つのは避けてください。Rust 2024エディションでは`static mut`への参照を作るだけで、既定でdenyの`static_mut_refs`がエラーを出します（rustc 1.99.1で確認。コンパイラは`&raw const`への置き換えを提案）。可変の共有状態は`AtomicU32`や`Mutex`、遅延初期化なら`OnceLock`で持ちます。

```
static VERSION: &str = "1.0"; // 静的領域に残る

fn main() {
    println!("{VERSION}");
}
```

## 型のサイズを size\_of で確かめる｜スタックに載る大きさの実測

値がスタックに占める大きさは[std::mem::size\_of](https://doc.rust-lang.org/std/mem/fn.size%5Fof.html)で確かめられます。ヒープに本体を置く型は、スタック側には「ポインタ（と長さ・容量）」しか残りません。Rust 1.99.1・x86\_64のmacOSで出力した値は次のとおりです。

```
use std::fmt::Display;
use std::mem::size_of;
use std::rc::Rc;

fn main() {
    println!("{}", size_of::<i32>());              // 4
    println!("{}", size_of::<Box<i32>>());         // 8
    println!("{}", size_of::<Box<[u8; 1024]>>());  // 8
    println!("{}", size_of::<Box<[u8]>>());        // 16
    println!("{}", size_of::<Box<dyn Display>>()); // 16
    println!("{}", size_of::<String>());           // 24
    println!("{}", size_of::<Vec<u8>>());          // 24
    println!("{}", size_of::<Rc<i32>>());          // 8
    println!("{}", size_of::<Option<Box<i32>>>()); // 8
}
```

| 型                            | スタック上のサイズ | 中身                       |
| ---------------------------- | --------- | ------------------------ |
| `Box<i32>`・`Box<[u8; 1024]>` | 8バイト      | ポインタ1つ                   |
| `Box<[u8]>`・`Box<dyn Trait>` | 16バイト     | ポインタ＋長さ（またはvtable）※現行の実装 |
| `String`・`Vec<u8>`           | 24バイト     | ポインタ＋長さ＋容量               |
| `Option<Box<i32>>`           | 8バイト      | `None`をヌルで表す             |

中身が1KiBでも`Box`は8バイトです。これが後述する「大きなバリアントを`Box`で包むとenum全体が縮む」理由になります。`Option<Box<T>>`が`Box<T>`と同じ8バイトなのは、ヌルポインタ最適化によるもので、[std::optionのドキュメント](https://doc.rust-lang.org/std/option/index.html#representation)がこのサイズの一致を保証しています。

## 値がヒープへ脱出する3つの条件｜Box・再帰型・大きなデータ

ここでは、スタックに置けない、あるいは置くと困る値を`Box`などでヒープへ移すことを**脱出**と呼びます。脱出が必要になるのは次の3つで、どれにも当てはまらない値はスタックのままで構いません。

### 条件1：dyn Traitなどサイズが決まらない型

トレイトオブジェクト（`dyn Trait`）やスライス（`[T]`）は、中身の型や要素数が実行時まで決まらないため、値のまま変数や戻り値にできません。`Box<dyn Trait>`にすると、スタックには16バイトのポインタだけが残り、本体はヒープに置かれます。分岐によって返す型が変わる関数は、この形が定番です。

```
trait Shape { fn area(&self) -> f64; }
struct Circle(f64);
struct Square(f64);
impl Shape for Circle { fn area(&self) -> f64 { 3.14 * self.0 * self.0 } }
impl Shape for Square { fn area(&self) -> f64 { self.0 * self.0 } }

// 返す型が実行時の分岐で決まる＝サイズが確定しないので Box で包む
fn make(kind: &str) -> Box<dyn Shape> {
    if kind == "circle" { Box::new(Circle(1.0)) } else { Box::new(Square(2.0)) }
}

fn main() {
    let shapes = vec![make("circle"), make("square")];
    for s in &shapes { println!("{}", s.area()); } // 3.14 と 4
}
```

### 条件2：自分自身を含む再帰的な型

連結リストや木のように、型の定義に自分自身が入っていると、サイズが無限に膨らみます。`Box`を挟まずに書くと、rustc 1.99.1は次のエラーで止まり、`Box`・`Rc`・`&`のどれかを挟むよう提案します。

```
error[E0072]: recursive type `List` has infinite size
 --> rec.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
```

`Box<List>`にすると、その部分はポインタ1つぶんに固定され、`List`全体は16バイトに収まります。次の要素はヒープへ脱出します。

```
enum List {
    Cons(i32, Box<List>),
    Nil,
}
use List::{Cons, Nil};

fn sum(l: &List) -> i32 {
    match l { Cons(v, next) => v + sum(next), Nil => 0 }
}

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("sum={} size={}", sum(&list), std::mem::size_of::<List>()); // sum=6 size=16
}
```

### 条件3：スタックに置くと溢れる大きなデータ

数MB単位の配列やバッファは、スタックに置いた時点でスレッドの容量を超えます。生成したスレッドの既定スタックは2MiBなので、そのスレッドでは2MiBを超える配列をローカル変数にできません（メインスレッドもmacOSの既定は8MiB）。こうしたデータは最初からヒープに確保します。一方で、数十バイトの構造体まで`Box`で包むのは逆効果です。確保のたびにアロケータを呼び、参照のたびにポインタをたどるぶん遅くなります。**小さな値はスタック、サイズ不定・再帰・巨大のときだけヒープ**と割り切ってください。

## 所有権とdrop｜ヒープのメモリが解放されるタイミング

脱出した値をいつ解放するかは、所有権が決めます。値の所有者は常に1つで、**所有者がスコープを抜けた時点で`drop`が呼ばれ、ヒープが返されます**。ガベージコレクタも手書きの`free`もありません。所有権・借用・ライフタイムの基礎は[Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)で扱っています。

### ムーブとCopy：代入でコピーされる24バイトと所有権の移動

ヒープに本体を持つ`String`や`Vec`を別の変数に代入すると、スタック上の24バイト（ポインタ・長さ・容量）だけがコピーされ、所有権が移ります（ムーブ）。元の変数は使えなくなるので、同じヒープを2回解放することは起きません。`i32`や`bool`のように`Copy`を実装する型は、代入で値そのものが複製され、元の変数も使い続けられます。

```
fn main() {
    let s = String::from("hello"); // 本体をヒープに確保
    let moved = s;                  // 所有権が moved へ移る。s はもう使えない
    println!("{moved}");
}                                   // moved がスコープを抜け、ここでヒープを解放
```

### Rc・Arcの循環参照とWeak

1つの値を複数の場所から持ちたいときは、参照カウントの`Rc<T>`（単一スレッド）か`Arc<T>`（複数スレッド）を使い、カウントが0になった時点で解放されます。ただし`Rc`同士が強参照で指し合うとカウントが0にならず、**メモリリーク**になります。リークはメモリ安全性の違反ではないため、コンパイラは止めません（[The Rust Programming Language 日本語版「循環参照は、メモリをリークすることもある」](https://doc.rust-jp.rs/book-ja/ch15-06-reference-cycles.html)）。親→子は`Rc`、子→親は`Weak`にして循環を断ちます。

```
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    name: &'static str,
    parent: RefCell<Weak<Node>>,      // 子→親は Weak
    children: RefCell<Vec<Rc<Node>>>, // 親→子は Rc
}

impl Drop for Node {
    fn drop(&mut self) { println!("drop {}", self.name); }
}

fn main() {
    let parent = Rc::new(Node { name: "parent", parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
    let child = Rc::new(Node { name: "child", parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
    *child.parent.borrow_mut() = Rc::downgrade(&parent);
    parent.children.borrow_mut().push(Rc::clone(&child));
    println!("parent strong={} weak={}", Rc::strong_count(&parent), Rc::weak_count(&parent));
    println!("child  strong={} weak={}", Rc::strong_count(&child), Rc::weak_count(&child));
}
```

実行すると`parent strong=1 weak=1`・`child strong=2 weak=0`と出たあと、`drop parent`・`drop child`の順で両方が解放されました。子→親も`Rc`にすると、どちらの`drop`も呼ばれません。リーク全般の調べ方は[メモリリークとは？原因の特定方法と直し方を言語・環境別に解説](/tech/details/3621/)にまとめています。

## スタックオーバーフローの原因と防ぎ方｜大きな配列の実測

Rustはメモリ安全ですが、スタックオーバーフローは`unsafe`なしで普通に起きます。溢れると、Rustの標準ライブラリが次のメッセージを出してプロセスを中断します（SIGABRT・終了コード134）。C言語では同じ状況がSIGSEGV（同じmacOSでの再帰テストは終了コード139）としてしか報告されず、Rustのように原因がスタックだとは表示されません。C/C++側の落ち方は[セグメンテーションフォルト（Segmentation fault）の原因と調べ方](/tech/details/7982/)で扱っています。

```
thread 'main' (49662405) has overflowed its stack
fatal runtime error: stack overflow, aborting
```

### Box::new(\[0u8; N\])がデバッグビルドで溢れる理由

`Box::new`は普通の関数なので、[Rust Referenceの評価順序](https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands)どおり、引数の配列は呼び出しより先に評価され、呼び出し側のスタックに組み立てられてからヒープへコピーされます。最適化でこのコピーが消えることはありますが、当てにはできません。[rust-lang/rustのissue #53827](https://github.com/rust-lang/rust/issues/53827)にも、`Box::new([-1; 3000000])`がスタックオーバーフローする再現例が報告されています。16MiBの配列をヒープへ置く4つの書き方を、メインスレッド（macOSの`ulimit -s`は8192KB）で実行した結果です。

| 書き方（N = 16MiB）                    | debugビルド          | releaseビルド |
| --------------------------------- | ----------------- | ---------- |
| `Box::new([0u8; N])`              | オーバーフロー（終了コード134） | 成功         |
| `vec![0u8; N].into_boxed_slice()` | 成功                | 成功         |
| `vec!`→`try_into`で`Box<[u8; N]>`  | 成功                | 成功         |
| `Box::new_zeroed()`               | 成功                | 成功         |

releaseで通るからといって`Box::new`のままにしないでください。テストやデバッグビルドでだけ落ちる、最適化の具合で落ちたり落ちなかったりする、という追いにくい不具合になります。

もう1つ注意点があります。最初はこの4つを1つの`main`の`match`に並べて実行すると、**実行しない`Box::new`の分岐があるだけで、`vec!`の分岐を選んでもdebugビルドで溢れました**。スタックフレームの大きさは関数の入口でまとめて確保されるので、同じ関数のどこかに巨大な一時値があれば、その分岐を通らなくても溢れます。表の結果は、書き方ごとに`#[inline(never)]`の関数へ分けて測り直したものです。

### 大きな配列をヒープへ直接置く3つの書き方

どれも呼び出し側のスタックに配列を作らず、ヒープへ直接確保します。

```
const N: usize = 16 * 1024 * 1024;

fn main() {
    // 1. 長さ可変のスライスでよいなら
    let a: Box<[u8]> = vec![0u8; N].into_boxed_slice();

    // 2. 型で長さを固定したいなら（Vec から変換）
    let b: Box<[u8; N]> = vec![0u8; N].into_boxed_slice().try_into().unwrap();

    // 3. ゼロ初期化でよいなら（Rust 1.92.0 で安定化）
    let c: Box<[u8; N]> = unsafe { Box::<[u8; N]>::new_zeroed().assume_init() };

    println!("{} {} {}", a.len(), b.len(), c.len());
}
```

迷ったら1の`vec!`で十分です。`Vec`のまま持てば`into_boxed_slice`すら要りません。3の[Box::new\_zeroed](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.new%5Fzeroed)はゼロのビット列が正しい値になる型（整数や浮動小数点の配列など）に限って使えます。参照や`NonNull`を含む型に対して`assume_init`すると未定義動作です（`new_zeroed`の呼び出し自体は安全）。

### Clippyのlarge\_stack\_arrays・large\_stack\_framesによる事前検出

Clippyでスタック上の巨大な値を見つける主なlintは次の3つです（ほかに`async`のFutureの大きさを見る`large_futures`もあります）。既定で警告が出るのは`large_enum_variant`だけで、残り2つは明示的に有効にしないと黙っています。閾値は[clippy\_configの既定値](https://github.com/rust-lang/rust-clippy/blob/rust-1.99.0/clippy%5Fconfig/src/conf.rs)から確認しました。

| lint                 | グループ            | 既定の閾値      | 検出するもの      |
| -------------------- | --------------- | ---------- | ----------- |
| `large_stack_arrays` | pedantic（既定は無効） | 16,384バイト  | ローカルの巨大配列   |
| `large_stack_frames` | nursery（既定は無効）  | 512,000バイト | 巨大なスタックフレーム |
| `large_enum_variant` | perf（既定で警告）     | 200バイト     | バリアント間のサイズ差 |

```
cargo clippy -- -W clippy::large_stack_arrays -W clippy::large_stack_frames
```

表の計測に使った`Box::new([0u8; N])`だけの関数にかけると、`warning: this function may allocate 16777264 bytes on the stack`と`allocating a local array larger than 16384 bytes`が出ました。上に載せた3つの書き方には何も出ません。CIで`-W`を付けておけば、debugビルドでしか落ちない配列をレビュー前に拾えます。Cargoの基本操作は[Cargo（Rust）とは？インストールと使い方・主要コマンド一覧](/tech/details/9572/)を参照してください。

## スタックサイズを変更する｜スレッド既定2MiBとRUST\_MIN\_STACK

[std::threadのドキュメント](https://doc.rust-lang.org/std/thread/index.html#stack-size)によると、生成したスレッドのスタックは現在すべてのTier 1プラットフォームで2MiBです（将来変わりうると明記）。変える方法は2つで、`Builder::stack_size`を指定するか、環境変数`RUST_MIN_STACK`にバイト数を入れます。両方あれば`Builder::stack_size`が優先です。**メインスレッドのスタックサイズはRustが決めない**とも書かれており、macOSやLinuxではシェルの`ulimit -s`（多くの環境で8MiB）に従います。`ulimit -s 65520`にしてから実行すると、debugビルドの`Box::new([0u8; N])`（16MiB）も通りました。

各フレームに1KiBの配列を持たせた再帰関数（表の下のコード）を、releaseビルドで深さを変えて実行した結果です。

| スレッドの作り方                       | 深さ     | 結果      |
| ------------------------------ | ------ | ------- |
| 既定（2MiB）                       | 1,500  | 成功      |
| 既定（2MiB）                       | 2,000  | オーバーフロー |
| `RUST_MIN_STACK=8388608`（8MiB） | 3,000  | 成功      |
| `stack_size(8MiB)`             | 3,000  | 成功      |
| `stack_size(8MiB)`             | 50,000 | オーバーフロー |
| `stack_size(64MiB)`            | 50,000 | 成功      |

```
use std::hint::black_box;
use std::thread;

// 各フレームに1KiBの配列を持たせた再帰
fn depth(n: u64) -> u64 {
    let buf = black_box([0u8; 1024]);
    if n == 0 { return buf[0] as u64; }
    depth(n - 1) + buf[1] as u64
}

fn main() {
    let handle = thread::Builder::new()
        .stack_size(64 * 1024 * 1024) // 64MiB。この行を外すと既定の2MiB
        .spawn(|| depth(50_000))
        .unwrap();
    println!("{}", handle.join().unwrap());
}
```

同じコードをdebugビルドで動かすと、フレームが大きくなるため64MiBでも50,000段で溢れました。必要なスタックはビルド設定でも変わるので、拡張する量はreleaseとdebugの両方で確かめてください。

スタックを広げるのは対症療法です。入力の大きさに比例して深くなる再帰（木の走査、構文解析など）は、入力次第でいずれ溢れます。そうした処理はループと`Vec`で自前のスタックを持つ形に書き換えるのが根本対策で、スタックサイズの拡張は深さの上限が分かっている処理に限ってください。再帰とスタックの関係は[再帰関数とは？自分を呼ぶ関数の仕組みとスタック・末尾再帰・実務判断を解説](/tech/details/15264/)で詳しく扱っています。

## Rustのメモリ使用量を減らす｜enumのBox化とVecの容量

メモリ使用量を減らすとき、最初に見るべきは「大量に並ぶ値の1個あたりのサイズ」です。`size_of`が数バイト縮むだけでも、100万個並べば数MBの差になります。

### enumの巨大バリアントのBox化とRSSの差

enumのサイズは、いちばん大きいバリアントに合わせて決まります。1KiBのデータを持つバリアントが1つでもあれば、データを持たない`Ping`も1,025バイトを占めます。100万要素のうち1%だけが1KiBのデータを持つ`Vec`を作り、`/usr/bin/time -l`で最大RSSを比べました。

```
enum Msg { Ping, Data([u8; 1024]) }            // size_of = 1025
enum MsgBoxed { Ping, Data(Box<[u8; 1024]>) }  // size_of = 8
```

| 要素の型       | 1要素のサイズ  | 最大RSS（100万要素） |
| ---------- | -------- | ------------- |
| `Msg`      | 1,025バイト | 約1,025.7MB    |
| `MsgBoxed` | 8バイト     | 約19.1MB       |

大きなバリアントがまれにしか現れないなら、`Box`で包むだけで約54分の1になりました。逆に、ほとんどの要素が大きなバリアントなら、要素ごとのヒープ確保が増えるだけなので包む意味はありません。Clippyの`large_enum_variant`が既定で警告するのは、最大のバリアントと2番目に大きいバリアントのサイズ差が200バイトを超えたときで、この`Msg`も警告の対象でした。

### Vecの容量を見直す：with\_capacityとshrink\_to\_fit

`Vec`は`push`で容量が足りなくなると、より大きな領域を確保し直します。5,000万個の`u64`を`push`で積んだ場合、最後の容量は67,108,864要素ぶん（約537MB）で、格納している5,000万要素より約1,700万要素多く予約していました。`Vec::with_capacity(50_000_000)`で始めると容量はぴったり5,000万です。ただし、このmacOS環境では最大RSSはどちらも約401MBで差が出ませんでした。触っていない予約ぶんは物理メモリに載らないためです。差が効いてくるのは、そのあと要素を書き足したときや、長く保持するときです。件数が分かっているなら`with_capacity`、作り終えたあと長く持つなら`shrink_to_fit`で容量を`len`に合わせます。

### メモリ使用量の測り方：/usr/bin/timeの最大RSS

プロセス全体の最大RSSなら、追加ツールなしで測れます。

```
# macOS：「maximum resident set size」がバイト単位で出る
/usr/bin/time -l ./target/release/app

# Linux（GNU time）：「Maximum resident set size (kbytes)」が出る
/usr/bin/time -v ./target/release/app
```

どの型がどれだけ確保しているかまで知りたいときは、ヒーププロファイラ（Linuxならheaptrack、Rustのコードに組み込むなら`dhat`クレート）に進みます。非同期ランタイムを使うサーバーなら、タスクごとに持つ状態のサイズも効く要素です。`async`の仕組みは[Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](/tech/details/8681/)で扱っています。

## よくある質問

### Rustでスタックとヒープはどう使い分けますか？

コンパイル時にサイズが決まる値はスタック、伸び縮みするデータや、`dyn Trait`・再帰型・数MB単位の配列はヒープです。迷ったらスタックのままにし、脱出の3条件に当てはまったときだけ`Box`や`Vec`を選びます。

### Rustでは値が自動でヒープへ移動することはありますか？

ありません。Goのようなエスケープ解析で、コンパイラが値をヒープへ移すことはしません。ヒープを使うのは、`Box`・`Vec`・`String`・`Rc`など、内部でアロケータを呼ぶ型を使ったときだけです。

### Box::newで大きな配列を作るとスタックオーバーフローするのはなぜですか？

引数の配列がいったん呼び出し側のスタックに作られ、そのあとヒープへコピーされるためです。Rust 1.99.1で16MiBの配列を試すと、debugビルドでは溢れ、releaseビルドでは最適化でコピーが消えて通りました。`vec![0u8; N]`か`Box::new_zeroed()`に置き換えれば、どちらのビルドでも通ります。同じ関数の中に`Box::new([0u8; N])`が残っていると、その分岐を通らなくてもdebugビルドで溢れるので、書き換え漏れに注意してください。

### Rustのスタックサイズはどうやって変更しますか？

生成するスレッドなら`thread::Builder::new().stack_size(バイト数)`か、環境変数`RUST_MIN_STACK`で指定します。既定は2MiBです。メインスレッドのサイズはOS側の設定（`ulimit -s`など）で決まるため、深い再帰は別スレッドを立てて実行するのが確実です。

### 変数がスタックとヒープのどちらにあるか確認する方法はありますか？

型を見るのが基本で、`std::mem::size_of`でスタック上の大きさを確かめられます。64ビット環境では`Box<i32>`は8バイト、`String`と`Vec`は24バイトで、どれも本体はヒープにあり、スタックにはポインタなどの管理情報だけが載っています。

## 関連記事

- [Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)
- [再帰関数とは？自分を呼ぶ関数の仕組みとスタック・末尾再帰・実務判断を解説](/tech/details/15264/)
- [メモリリークとは？原因の特定方法と直し方を言語・環境別に解説](/tech/details/3621/)
- [セグメンテーションフォルト（Segmentation fault）の原因と調べ方｜core dumpedの意味・gdb・ASan](/tech/details/7982/)
- [Cargo（Rust）とは？インストールと使い方・主要コマンド一覧](/tech/details/9572/)

---

出典: [Rustのスタックとヒープ｜値がヒープへ脱出する条件とスタックオーバーフローの防ぎ方](<https://www.issoh.co.jp/tech/details/2814/>)（株式会社一創）
