前言
学习高阶程序语言通常都会接受一个观念是:「没用到的变量会自动被垃圾回收掉」。不过越接近底层或开始探讨性能问题,发现自己对于程序语言核心的记忆体概念 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