目錄
- 行程的記憶體配置
- Stack(堆疊)
- Heap(堆積)
- Stack vs Heap 核心差異
- 變數住在哪?
- 值 vs 參考
- 生命週期與記憶體回收
- 常見記憶體問題
- 各語言怎麼處理
- 與執行緒的關係
- 常見問題
- 總結
行程的記憶體配置
先搞懂「位址」與高低
記憶體可以想成一長排有編號的格子,每格 1 byte,編號從 0 開始往上排(0、1、2…一直到很大)。這個編號就是位址(address):
- 低位址 = 編號小的格子(靠近 0)
- 高位址 = 編號大的格子(數字很大那端)
下圖只是把這排格子直立畫:底部是低位址、頂部是高位址。重點看箭頭——Stack 在高位址往下長、Heap 在低位址往上長,兩者朝彼此成長,中間留一大段空隙當緩衝,讓兩邊都能彈性增長、晚點才可能相撞。
不必背「誰在上誰在下」——那是平台慣例(不同系統可能不同)。真正重要的是後面的觀念:stack 自動管理、heap 要回收。
一個執行中的行程,其虛擬記憶體大致分成幾個區段(由低位址到高位址):
高位址 ┌──────────────────┐
│ Stack(堆疊) │ ↓ 向下成長:函式呼叫、區域變數
│ ⋮ │
│ (空隙) │
│ ⋮ │
│ Heap(堆積) │ ↑ 向上成長:動態配置的資料
├──────────────────┤
│ BSS / Data │ 全域 / 靜態變數
├──────────────────┤
低位址 │ Text / Code │ 程式指令(唯讀)
└──────────────────┘
| 區段 | 存放 |
|---|---|
| Text(程式碼段) | 編譯後的機器指令,唯讀 |
| Data / BSS | 全域變數、靜態變數 |
| Heap(堆積) | 執行期動態配置的資料 |
| Stack(堆疊) | 函式呼叫框架、區域變數 |
本篇聚焦最常被拿來比較、也最常搞混的 Stack 與 Heap。
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 記憶體管理與 JVM、Go 指標完全指南。
與執行緒的關係
這點把記憶體接回並發:
Process
┌──────── Heap(所有執行緒共享)────────┐
│ │
Thread 1 Thread 2 Thread 3
(自己的 stack) (自己的 stack) (自己的 stack)
- 每條執行緒有自己獨立的 stack——區域變數天生執行緒安全(別人看不到)
- Heap 由行程內所有執行緒共享——共享物件存取需要同步,否則 race condition
常見問題
問題 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