メモリレイアウト

プログラムのメモリ空間の構造で、スタック、ヒープ、コード領域、データ領域から構成される

低レベルメモリ管理

メモリレイアウトとは

プログラムのメモリ空間は、コード領域、データ領域、ヒープ、スタックの 4 つの領域で構成される。JavaScript 開発者が直接操作することは少ないが、パフォーマンスやメモリリークの理解に重要。

メモリ領域

メモリ領域を図で示す。

高アドレス ┌──────────────┐
          │   スタック ↓   │ ← 関数の引数、ローカル変数
          │              │
          │   ↓ 成長方向  │
          │              │
          │   ↑ 成長方向  │
          │              │
          │   ヒープ ↑    │ ← 動的に確保されるオブジェクト
          ├──────────────┤
          │   データ領域   │ ← グローバル変数、定数
          ├──────────────┤
          │   コード領域   │ ← 実行可能なコード
低アドレス └──────────────┘
領域内容管理
スタック関数の引数、ローカル変数自動 (LIFO)
ヒープオブジェクト、配列、文字列GC or 手動
データグローバル変数、定数コンパイル時に確定
コード実行可能な命令読み取り専用

スタック vs ヒープ

スタックとヒープの違いを以下にまとめる。

観点スタックヒープ
速度高速 (ポインタ移動のみ)低速 (アロケーション)
サイズ小さい (数 MB)大きい (数 GB)
管理自動 (関数終了で解放)GC or 手動
格納データプリミティブ、参照オブジェクト、配列

V8 (Node.js) のメモリ

V8 (Node.js) のメモリのコード例を示す。

// スタック: プリミティブ値と参照
const x = 42;           // スタックに 42 を格納
const obj = { a: 1 };   // スタックに参照、ヒープにオブジェクト

// ヒープ: オブジェクト
// V8 のヒープは Young Generation と Old Generation に分かれる

LambdaMemorySize はヒープ、スタック、OS のオーバーヘッドを含む総量で、128 MB から 10,240 MB の範囲で設定する。CPU はこの設定量に比例して割り当てられ、公式ドキュメントの記載では 1,769 MB で 1 vCPU 相当になる。したがって 1,769 MB より下の領域ではメモリを増やすほど計算自体が速くなるが、それを超えた分は 2 つ目以降の vCPU として渡されるため、並列化していないコードでは増やしても速くならない。メモリ設定は実行時間あたりの単価に直接効くので、実際の関数で数点測り、所要時間と課金額の釣り合う値を選ぶ。

スタックオーバーフロー

スタックオーバーフローのコード例を示す。

// ❌ 無限再帰 → スタックオーバーフロー
function infinite(): number { return infinite(); }
// RangeError: Maximum call stack size exceeded

// ✅ 末尾呼び出しの形 (ただし V8 は最適化しないのでループの方が確実)
function factorial(n: number, acc = 1): number {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}

末尾再帰は「関数の最後の処理が自分自身の呼び出しだけ」という形で、理屈の上ではスタックを積み増さずに実行できる。ただし V8 (Node.js・Chrome) はこの最適化を実装していないため、上の factorial も引数を大きくすればスタックを使い切る。Node.js v26 で 100000 を渡すと RangeError: Maximum call stack size exceeded になり、素朴な再帰で辿れる深さは 1 万段前後だった (上限は環境と関数のフレームサイズで変わる)。再帰の深さが入力次第で伸びる処理は、書き方を工夫するのではなく while ループか明示的なスタック配列へ書き換えるのが確実である。

メモリリークの検出

メモリリークの検出の例を示す。

# Node.js のヒープ使用量を確認
node -e "console.log(process.memoryUsage())"
# { rss: 47988736, heapTotal: 6176768, heapUsed: 3847112, external: 1421601, arrayBuffers: 14573 }

返る値はすべてバイト単位の数値なので、人が読むときは 1024 で 2 回割る。rss はプロセス全体が OS から借りている物理メモリ、heapTotal は V8 がヒープとして確保した枠、heapUsed はそのうち実際に使われている分である。リークの疑いを見るときは 1 回の値ではなく推移を追う。負荷をかけた後に heapUsed が下がりきらず rss も戻らないなら、参照が残って回収されていないオブジェクトを疑う。

メモリレイアウトについては関連書籍でも詳しく扱われている。

この記事は役に立ちましたか?

関連用語

関連する記事