目录
什么是栈
栈是每个线程私有、按先进后出管理的调用临时区。
什么是堆
堆是进程共享、由内存分配器/GC统一管理的动态内存区。
栈 vs 堆(核心区别)
归属:栈是“每个线程一个栈”;堆是“整个进程(多线程)共享一大片内存”
用途:栈放调用过程相关的数据(返回地址、保存寄存器、局部变量等);堆放动态创建、可跨函数/长期存在的数据
生命周期:栈随函数返回自动回收(栈帧弹出);堆由程序(
free/delete
)或垃圾回收器回收分配/释放成本:栈是简单的指针移动,极快;堆需要向分配器申请/释放,相对慢
访问局部性:栈一般连续,缓存友好;堆可能碎片化
常见错误:栈——递归太深/局部数组过大导致栈溢出;堆——内存泄漏/重用已释放内存/碎片
大小:栈通常较小且固定/可增长(按语言/平台而定);堆通常大很多(由 OS/运行时管理)
GO编译器的逃逸分析
go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。 go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。
什么是逃逸分析?
编译器在编译期判断一个变量是否会在当前函数返回后仍被引用(“逃出”当前栈帧)。
不逃逸 → 尽量放在栈上,分配/回收极快;
逃逸 → 放到堆上,由运行时/GC 管理,带来分配与 GC 成本。
怎么看逃逸分析结果?
在你的包目录运行(推荐关掉内联更好读):
go build -gcflags='all=-m -l' ./...
# 或者对测试/基准:
go test -run=^$ -bench=. -gcflags='all=-m -l' ./...
常见输出含义:
moved to heap: x
/escapes to heap
:变量x
上堆了does not escape
:未逃逸(可栈上)leaking param
/leaking param content
:把参数或其内容泄露到了可能长寿命的位置(导致逃逸)
典型“会逃逸”的场景
返回局部变量的地址/引用
func f1() *int {
x := 10
return &x // x 逃逸:必须活到函数返回之后
}
闭包捕获局部变量
func f2() func() {
x := 0
return func() { x++ } // x 被闭包捕获 → 逃逸
}
返回或保存带有“底层存储”的容器
func f3(n int) []int {
s := make([]int, n)
return s // s 的底层数组需在返回后仍然存活 → 逃逸
}
经由接口/反射/fmt 等导致装箱或被长期保存
func f4(b []byte) {
_ = fmt.Sprintf("%x", b) // b 常见会逃逸(fmt 可变参、接口装箱)
}
把指针/引用存入全局、堆对象或长生命周期结构
var g []*T
func f5(p *T) {
g = append(g, p) // p 的“内容”被长期保存 → 逃逸
}
参数“内容”被函数保留(编译器推不动)
func keepPtr(pp **int) { stash = *pp } // 比如存到包级变量
func caller() {
x := 1
p := &x
keepPtr(&p) // 报 "leaking param content: p"
}
GO中的“堆/栈”怎么落地
落地
语法上没有显式“栈/堆”关键字;逃逸分析决定变量放栈还是堆
一般规律:不逃逸的局部数据可在栈上;返回到函数外/被闭包捕获等会逃逸到堆
make
(slice/map/channel)和new
只是创建方式,是否上堆由逃逸分析决定,堆内存由 GC 回收
是否上堆由逃逸分析决定
type T struct { buf [1024]byte }
func f() *T {
t := T{} // 语义上是局部变量;若返回其地址 => 逃逸到堆(由编译器决定)
return &t
}
func g() {
t := T{} // 不逃逸的话,可能在栈上分配,函数返回就回收
_ = t
}
什么时候“应该”用谁?
短生命周期、只在当前调用链内使用:栈(语言通常自动用栈)
需要跨函数/跨协程/长期缓存:堆(动态分配)
在拥有 GC 的语言(Go/Java/Python)中,写法更关注语义,由编译器/运行时决定最终放栈还是堆;只需要留意可能引发逃逸的用法和不必要的大对象分配。