# Golang 内存模型和垃圾回收 ## 问题 1. Go 的内存分配策略是什么? 2. Go 的垃圾回收(GC)是如何工作的? 3. 什么是逃逸分析? 4. Go 的内存泄漏常见场景有哪些? 5. sync.Pool 的作用是什么? 6. 如何优化 Go 程序的内存使用? --- ## 标准答案 ### 1. 内存分配策略 #### **堆和栈** **栈(Stack)**: - 存储局部变量 - 自动分配和释放 - 速度快 ```go func foo() int { x := 1 // 分配在栈上 return x } // x 自动释放 ``` --- **堆(Heap)**: - 存储动态分配的数据 - 需要 GC 回收 ```go func foo() *int { x := 1 return &x // x 逃逸到堆上 } ``` --- #### **逃逸分析** **定义**:编译器分析变量的生命周期,决定分配在栈还是堆。 **示例**: ```go // ❌ 逃逸到堆 func foo() *int { x := 1 return &x // x 的引用被返回,必须分配在堆上 } // ✅ 不逃逸,分配在栈上 func bar() int { x := 1 return x // x 的值被返回,可以分配在栈上 } ``` --- ### 2. Go 的垃圾回收 #### **GC 算法:三色标记法 + 并发标记清理** **三色标记**: - **白色**:未访问 - **灰色**:已访问,但引用的对象未全部访问 - **黑色**:已访问,引用的对象也全部访问 **流程**: ``` 1. 根节点标记为灰色 2. 遍历引用对象,标记为灰色 3. 递归标记,直到所有可达对象为黑色 4. 清扫白色对象(垃圾) ``` --- #### **GC 触发条件** ```go // 环境变量 // GOGC:触发 GC 的堆增长率(默认 100) // GOMEMLIMIT:触发 GC 的堆内存上限 // 示例 GOGC=100 // 堆增长 100% 时触发 GC GOMEMLIMIT=8192 // 堆上限 8 GB ``` --- ### 3. 常见内存泄漏场景 #### **场景 1:Goroutine 泄漏** ```go // ❌ 泄漏 func leak() { for i := 0; i < 100; i++ { go func() { time.Sleep(time.Hour) // 永不退出 }() } } ``` **解决**: ```go func noLeak() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() for i := 0; i < 100; i++ { go func(ctx context.Context) { select { case <-ctx.Done(): return case <-time.After(time.Hour): } }(ctx) } } ``` --- #### **场景 2:切片引用** ```go // ❌ 泄漏 func leak() { for i := 0; i < 1000; i++ { s := make([]int, 1024) _ = append(s, i) // s 被全局变量引用,无法 GC } } ``` --- ### 4. sync.Pool #### **作用** **对象池**:复用对象,减少 GC 压力 --- #### **示例** ```go var pool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) }, } func process() { // 从池中获取 buf := pool.Get().([]byte) // 使用 // ... // 归还到池中 pool.Put(buf) } ``` --- #### **适用场景** - 频繁创建的对象([]byte、Buffer) - 临时对象(减少 GC) - 对象创建成本高 --- ### 5. 内存优化技巧 #### **1. 预分配切片容量** ```go // ❌ 多次扩容 s := []int{} for i := 0; i < 1000; i++ { s = append(s, i) // 多次扩容和拷贝 } // ✅ 预分配 s := make([]int, 0, 1000) for i := 0; i < 1000; i++ { s = append(s, i) // 无扩容 } ``` --- #### **2. 使用 strings.Builder** ```go // ❌ 字符串拼接(内存拷贝) s := "" for i := 0; i < 1000; i++ { s += fmt.Sprintf("%d", i) // 每次都创建新字符串 } // ✅ strings.Builder var b strings.Builder for i := 0; i < 1000; i++ { b.WriteString(strconv.Itoa(i)) } s := b.String() // 只分配一次 ``` --- #### **3. 避免不必要的指针** ```go // ❌ 使用指针(增加 GC 扫描时间) type User struct { Name *string } // ✅ 使用值类型 type User struct { Name string } ``` --- ### 6. 阿里 P7 加分项 **深度理解**: - 理解 Go 的内存分配策略(栈、堆、逃逸分析) - 理解 Go GC 的三色标记法 - 理解 GC 的触发条件和调优参数 **实战经验**: - 有使用 pprof 进行内存分析的经验 - 有优化内存使用的经验(减少 GC、复用对象) - 有处理内存泄漏的经验 **性能优化**: - 理解如何减少内存分配(sync.Pool、预分配) - 理解如何减少 GC 压力(避免指针、复用对象) - 理解如何使用 pprof 进行性能分析