前言
學習高階程式語言通常都會接受一個觀念是:「沒用到的變數會自動被垃圾回收掉」。不過越接近底層或開始探討效能問題,發現自己對於程式語言核心的記憶體概念 Heap 與 Stack 並沒有那麼清楚。程式語言究竟是如何分配與管理記憶體的?所謂的垃圾回收(Garbage Collection, GC)具體來說又做了哪些事情?
為什麼需要 Stack 與 Heap?
程式運行時需要將變數存放於記憶體當中,而作業系統作為程式與硬體之間協調的媒介,Stack 與 Heap 正是作業系統與語言執行環境用來管理記憶體的架構:
- 效率:現代 CPU 為了效能會使用快取來暫存常用的記憶體片段,當記憶體內容變動時則必須同步更新快取,而這個過程本身是有成本的,從效能角度來看,資料在記憶體中越連續緊湊,CPU 快取命中率就越高,效能也越好。透過利用 Stack 可預期的記憶體分配方式,來換取極高的存取效率。
- 靈活性:基於需要動態改變內容的資料與大量存放的需求,透過 Heap 靈活存放的特性互補 Stack 的弱項。
Stack
Stack 是一種「後進先出」的資料結構用於存放函式運行中的資訊,想像一層層鬆餅堆疊後從最上面開始吃,最後放上去的最先被吃掉,且只能 push(推入)與 pop (彈出)來改變內容,運行原理如下:
- 系統會在 Stack 上建立一個新的 Stack Frame
- 把該函式的參數、區域變數、返回地址、暫存器狀態⋯⋯等資料放進去
- 函式執行完畢後,整個 frame 會一次被 pop(彈出)
- 優點:
- 快速頻繁的操作:結構上不需要找位置或判斷資料還有沒有在使用,可預期的結果造就高操作效率。
- 存放緊湊:運用記憶體緊湊方便快取。
- 缺點:
- 無法應對對於浮動尺寸的資料:結構上很難優雅的應對浮動尺寸的資料。
- 空間有限:結構上 Stack 需要指派固定尺寸,通常是數個 MB,可能在大量資料或遞迴場合下 Stack Overflow。
Heap
Process ├─ Thread A → Stack A ├─ Thread B → Stack B └─ Heap (shared)Heap 是一塊用來存放「動態分配資料」的記憶體區域,不具備嚴格的進出順序限制,可以採用不同的寫入策略(First Fit、Best Fit、Worst Fit)。
基於 Stack 較小的固定尺寸且寫入時只能緊湊的推入資料,Stack 通常僅保存指向 Heap 的指標,實際資料分配於 Heap,以避免 Stack 容量小且生命週期受限的問題。
- 優點:
- 靈活寫入與釋放:可動態的向 OS 申請更多空間。
- 可存放跨多個函式存活的資料:由於 Stack 容量固定且資料生命週期受限,當資料需要跨函式存活或大小在編譯期無法確定時,通常會分配於 Heap,而 Stack 僅保存指向 Heap 的指標。
- 缺點:
- 分配速度較慢:需要尋找合適的記憶體區塊來存放資料。
- 需要額外的管理機制:在高階語言中,這通常由垃圾回收器(GC)負責,在 C 語言中則透過:
malloc / calloc / realloc:向 Heap 分配記憶體。free:釋放已分配的記憶體。
- 可能產生記憶體碎片:長時間分配與釋放不同大小的區塊,會造成可用空間不連續。
Go 觀察逃逸到 Heap 的變數
Go 編譯器可以透過逃逸分析(Escape Analysis)來決定變數應該分配在 Stack 還是 Heap,當編譯器無法證明變數在函數返回後不會被使用時,變數就會「逃逸」到 Heap 上:
go build -gcflags="-m" main.go
./heap-test.go:5:5: moved to heap: x./heap-test.go:11:5: moved to heap: count./heap-test.go:20:9: moved to heap: arr./heap-test.go:27:12: 100 escapes to heappackage main
import "fmt"
// 範例 1: 返回局部變數的指針func createPointer() *int { x := 42 // x 原本在 stack 上 return &x // 返回指針導致 x 逃逸到 heap}
// 範例 2: 變數被閉包捕獲func createClosure() func() int { count := 0 // count 逃逸到 heap return func() int { count++ return count }}
// 範例 3: 變數太大func largeArray() *[10000]int { var arr [10000]int // 太大,逃逸到 heap return &arr}
// 範例 4: interface 類型func useInterface() interface{} { x := 100 // 賦值給 interface 導致逃逸 return x}
func main() { // 範例 1 ptr := createPointer() fmt.Println("指針值:", *ptr)
// 範例 2 counter := createClosure() fmt.Println("計數:", counter()) // 1 fmt.Println("計數:", counter()) // 2
// 範例 3 arr := largeArray() fmt.Println("陣列第一個元素:", arr[0])
// 範例 4 val := useInterface() fmt.Println("interface 值:", val)}延伸閱讀
- Memory Explained - Why Your Code Crashes (Stack vs Heap) - LearnThatStack
- Golang pointers explained, once and for all - JamieGo
- WHY IS THE STACK SO FAST? - Core Dumped
- WHY IS THE HEAP SO SLOW? - Core Dumped
- CPU Core / Process / Thread / Fiber / Coroutine 差異與解釋 - WebDong