Heap 與 Stack 記憶體完全指南

跨語言理解行程記憶體配置:Stack 與 Heap 的差異、變數住在哪、生命週期,以及常見記憶體問題


目錄


行程的記憶體配置

先搞懂「位址」與高低

記憶體可以想成一長排有編號的格子,每格 1 byte,編號從 0 開始往上排(0、1、2…一直到很大)。這個編號就是位址(address)

  • 低位址 = 編號小的格子(靠近 0)
  • 高位址 = 編號大的格子(數字很大那端)

下圖只是把這排格子直立畫:底部是低位址、頂部是高位址。重點看箭頭——Stack 在高位址往下長、Heap 在低位址往上長,兩者朝彼此成長,中間留一大段空隙當緩衝,讓兩邊都能彈性增長、晚點才可能相撞。

不必背「誰在上誰在下」——那是平台慣例(不同系統可能不同)。真正重要的是後面的觀念:stack 自動管理、heap 要回收

一個執行中的行程,其虛擬記憶體大致分成幾個區段(由低位址到高位址):

高位址  ┌──────────────────┐
       │   Stack(堆疊)   │  ↓ 向下成長:函式呼叫、區域變數
       │       ⋮          │
       │      (空隙)     │
       │       ⋮          │
       │   Heap(堆積)    │  ↑ 向上成長:動態配置的資料
       ├──────────────────┤
       │   BSS / Data     │  全域 / 靜態變數
       ├──────────────────┤
低位址  │   Text / Code    │  程式指令(唯讀)
       └──────────────────┘
區段 存放
Text(程式碼段) 編譯後的機器指令,唯讀
Data / BSS 全域變數、靜態變數
Heap(堆積) 執行期動態配置的資料
Stack(堆疊) 函式呼叫框架、區域變數

本篇聚焦最常被拿來比較、也最常搞混的 StackHeap


Stack(堆疊)

Stack 管理函式呼叫。每呼叫一個函式,就在 stack 頂端推入一個 堆疊框架(stack frame),存放該函式的區域變數、參數、回傳位址;函式回傳時整個框架彈出。

呼叫 a() → 呼叫 b() → 呼叫 c()

┌──────┐ ← stack 頂(c 的框架)
│ c()  │
├──────┤
│ b()  │
├──────┤
│ a()  │
└──────┘ ← stack 底
c 回傳 → c 框架彈出,回到 b

特點

  • LIFO(後進先出):呼叫 / 回傳天然符合堆疊
  • 自動管理:框架隨函式進入 / 離開自動配置 / 釋放,不需手動
  • 極快:配置只是移動堆疊指標(一個加減法)
  • 空間有限:大小固定(常見數 MB),遞迴太深或巨大區域變數會 Stack Overflow
  • 生命週期明確:區域變數活在函式執行期間,函式回傳即消失

Heap(堆積)

Heap 管理動態配置——大小或生命週期在編譯期無法決定、需在執行期才知道的資料。

// C:手動配置與釋放
int *p = malloc(sizeof(int) * 100);   // 在 heap 上要 100 個 int
free(p);                               // 用完手動釋放

特點

  • 手動 / GC 管理:C/C++ 手動 malloc/free;Java/Go/Python 由 垃圾回收(GC) 自動回收
  • 較慢:要尋找合適的空閒區塊、記錄配置資訊
  • 空間大:可用記憶體遠大於 stack
  • 生命週期自由:可跨函式存活,直到被釋放 / 回收
  • 會碎片化(fragmentation):頻繁配置 / 釋放不同大小,留下零散空洞

Stack vs Heap 核心差異

面向 Stack(堆疊) Heap(堆積)
用途 函式呼叫、區域變數 動態配置的資料
管理 自動(進出函式) 手動 或 GC
速度 極快(移指標) 較慢(找空閒區塊)
大小 小、固定(數 MB) 大、可成長
生命週期 函式執行期間 自由,直到釋放 / 回收
存取 各執行緒獨立 行程內共享
典型錯誤 Stack Overflow 記憶體洩漏、碎片化、懸空指標

變數住在哪?

關鍵不在「變數本身」,而在它的大小與生命週期能不能在編譯期決定

能在編譯期決定大小、生命週期隨函式 → Stack
大小 / 生命週期不定、需跨函式存活    → Heap

不同語言判斷方式不同:

  • C/C++:區域變數預設在 stack;malloc/new 配置在 heap
  • Java:基本型別(int、boolean…)的區域變數在 stack;物件一律在 heap,stack 上只放指向它的參考
  • Go:編譯器用逃逸分析(escape analysis) 決定——若變數的位址「逃出」函式(被外部引用),就配置在 heap,否則放 stack
  • Python:幾乎所有物件都在 heap(一切皆物件),變數名只是指向它的參考

重點:「在 stack 還是 heap」往往不是程式設計師直接控制,而是語言 / 編譯器依規則決定。


值 vs 參考

這直接對應 stack / heap:

值(Value):       資料直接放在變數的位置(常在 stack)
參考(Reference): 變數放的是「指向 heap 上資料的位址」
int x = 5;              // x 直接存 5(stack)
Object o = new Obj();   // o 存的是位址 → 指向 heap 上的物件

stack:  x=5    o=0x7f.. ──┐
heap:                     └─► [ Obj 實體 ]
  • 傳「值」→ 複製整份資料
  • 傳「參考」→ 複製位址,兩個變數指向同一份 heap 資料(改一個會影響另一個)

這是「為什麼把物件傳進函式、在裡面改它、外面也變了」的根本原因——傳的是參考,指向同一塊 heap。


生命週期與記憶體回收

Stack:自動且即時

函式回傳,框架彈出,區域變數立刻消失。無需任何回收機制。

Heap:需要被釋放或回收

heap 上的資料不會自己消失,必須:

  • 手動釋放(C/C++):free / delete——忘了釋放 → 記憶體洩漏;釋放後還用 → 懸空指標
  • 垃圾回收(GC)(Java/Go/Python…):runtime 自動找出「不再被任何參考指到」的物件並回收
GC 的核心問題:這塊 heap 資料還有人用嗎?
→ 沒有任何參考指向它 → 可回收

GC 省去手動釋放的負擔,但會帶來回收停頓與額外開銷;手動管理快但易出錯。這是一個經典取捨。


常見記憶體問題

問題 區域 原因
Stack Overflow Stack 遞迴太深 / 無窮遞迴、過大的區域變數
記憶體洩漏(Memory Leak) Heap 配置後未釋放(C/C++),或物件一直被參考、GC 無法回收
懸空指標(Dangling Pointer) Heap 釋放後仍存取那塊記憶體(C/C++)
重複釋放(Double Free) Heap 同一塊記憶體 free 兩次
碎片化(Fragmentation) Heap 頻繁配置 / 釋放不同大小,空閒空間零散

注意:有 GC 的語言也會記憶體洩漏——不是忘了 free,而是該放手的物件仍被參考(如靜態集合一直累積、未移除的監聽器)。


各語言怎麼處理

語言 Heap 管理 物件放哪 特點
C / C++ 手動 malloc/free 由程式決定 最快、最危險,責任在開發者
Java GC 物件一律 heap stack 放基本型別與參考
Go GC + 逃逸分析 編譯器決定 不逃逸就放 stack,省 GC 壓力
Python GC(引用計數 + 循環偵測) 幾乎全 heap 一切皆物件
Rust 所有權(無 GC) 由所有權規則 編譯期保證安全,免 GC

細節見 Java 記憶體管理與 JVMGo 指標完全指南


與執行緒的關係

這點把記憶體接回並發:

                Process
   ┌──────── Heap(所有執行緒共享)────────┐
   │                                       │
 Thread 1        Thread 2        Thread 3
 (自己的 stack) (自己的 stack)  (自己的 stack)
  • 每條執行緒有自己獨立的 stack——區域變數天生執行緒安全(別人看不到)
  • Heap 由行程內所有執行緒共享——共享物件存取需要同步,否則 race condition

這正是 程式、行程與執行緒 中「共享 heap、獨立 stack」的記憶體層解釋;同步機制見 Lock 鎖機制


常見問題

問題 1:Stack 為什麼比 Heap 快?

Stack 配置只是移動堆疊指標(一次加減法),釋放是函式回傳時自動彈出;Heap 要尋找合適空閒區塊、記錄配置資訊、之後還要釋放 / GC,步驟多得多。

問題 2:變數到底在 stack 還是 heap?我能控制嗎?

取決於語言規則與大小 / 生命週期是否編譯期可知。多數語言由編譯器 / runtime 決定(如 Java 物件必在 heap、Go 靠逃逸分析),程式設計師通常無法直接指定。

問題 3:什麼是 Stack Overflow?

stack 空間用盡,最常見是無窮遞迴或遞迴太深,每層呼叫都堆一個框架,超過 stack 上限就崩潰。

問題 4:有 GC 就不會記憶體洩漏了嗎?

不是。GC 只回收「沒有任何參考指向」的物件。若物件仍被參考(如一直往靜態 List 加東西、忘了移除監聽器),GC 不會回收它,照樣洩漏。

問題 5:為什麼物件傳進函式,改了外面也跟著變?

因為傳的是參考(指向 heap 上同一份資料的位址),不是複製整個物件。函式內外兩個變數指向同一塊 heap。

問題 6:每條執行緒共用同一個 stack 嗎?

不是。每條執行緒有自己獨立的 stack;它們共享的是 heap。所以區域變數天生安全,共享物件才需要同步。


總結

核心要點

  • 行程記憶體分 Text / Data / Heap / Stack;最常比較的是後兩者
  • Stack:函式呼叫框架、區域變數,自動管理、極快、空間小(→ Stack Overflow)
  • Heap:動態配置,手動或 GC 管理、較慢、空間大(→ 洩漏、碎片化、懸空指標)
  • 變數放哪由語言 / 編譯器依大小與生命週期決定,多半不由程式直接控制
  • vs 參考對應 stack 直接存資料 vs 存指向 heap 的位址
  • 每條執行緒獨立 stack、共享 heap——區域變數安全、共享物件需同步
  • GC 也會洩漏:物件「還被參考」就不會被回收

快速參考

Stack Heap
存什麼 區域變數、呼叫框架 動態配置的資料 / 物件
管理 自動 手動 / GC
速度 極快 較慢
大小 小、固定 大、可成長
執行緒 各自獨立 共享
錯誤 Stack Overflow 洩漏 / 碎片 / 懸空指標

建立日期:2026-06-18

🔗相關文章