# 一、 核心概念:栈 (Stack) vs 堆 (Heap)
| 特性 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 分配速度 | 极快(移动栈顶指针) | 较慢(需寻找合适内存块) |
| 管理方式 | 编译器自动分配与释放 | 由 GC (垃圾回收器) 管理释放 |
| 生存周期 | 随函数调用开始 / 结束 | 持续到不再被引用,由 GC 回收 |
| 碎片 | 无 | 容易产生碎片 |
# 二、 逃逸分析 (Escape Analysis)
Go 编译器会自动决定变量分配在栈还是堆。原则: 如果变量在函数结束后仍被引用,则必须逃逸到堆。
# 1. 指针逃逸
场景: 函数返回局部变量的地址。
func createPointer() *int { | |
x := 10 // 本地变量 x | |
return &x //x 逃逸:外部需要访问这个地址 | |
} |
# 2. Interface 逃逸
场景: 变量作为参数传给 interface{} 类型函数(如 fmt.Println )。
func main() { | |
name := "Larry" | |
fmt.Println(name) //name 会逃逸 | |
} | |
// 解析:fmt.Println 接收空接口,编译器无法在编译期确定其动态类型。 |
# 3. 闭包捕获逃逸
场景: 内部函数引用了外部函数的局部变量。
func increase() func() int { | |
n := 0 | |
return func() int { | |
n++ //n 逃逸:因为闭包的生命周期长于 increase 函数 | |
return n | |
} | |
} |
# 4. 动态大小或巨型变量
场景: 变量太大导致栈空间不足,或者切片长度在编译期无法确定。
func sliceEscape() { | |
// 长度是变量,编译期不确定,可能逃逸 | |
size := 10 | |
s := make([]int, size) | |
// 或者对象过大,如: | |
// bigObj := make([]int, 10000) | |
} |
# 三、 Go 内存分级布局 (TCmalloc 思想)
Go 内存分配器的三层架构(为了实现无锁分配,提高并发性能):
- mcache: 每个 P (Processor) 私有的缓存。无锁,速度最快。
- mcentral: 全局中央缓存。当 mcache 耗尽时,向其申请。有锁竞争。
- mheap: 堆内存总量。当 mcentral 不足时,向操作系统申请大块内存。
# 四、 面试高频追问
Q: 逃逸分析有什么好处?
A: 1. 减少堆内存分配,减轻 GC 负担。 2. 栈分配比堆分配效率更高。 3. 提高缓存命中率。
Q: 指针传递一定比值传递快吗?
A: 不一定。如果指针传递导致变量从栈逃逸到堆,那么堆分配和后续 GC 的开销可能远大于值拷贝的开销。
Q: 如何手动查看逃逸结果?
A: 执行
go build -gcflags="-m -l" main.go。
# 五、 实战建议 (周一复习指南)
- 对比实验:写出上述三个案例,运行
-gcflags="-m"命令观察输出。 - 阅读源码:如果精力允许,看一眼
runtime/malloc.go中关于mcache的注释。 - 结合并发:思考为什么
mcache放在P上而不是G上?(提示:P 保证了并发下的亲和性)。