Day 21|内存分配与 GC(垃圾回收)基础[Go 语言 30 天系统学习计划]

学习目标

  • 理解 Go 的内存分配模型
  • 区分栈(stack)与堆(heap)的职责
  • 理解 Go GC 的基本原理与运行阶段
  • 建立“面向 GC 编程”的工程意识

一、为什么需要理解内存与 GC

在 Go 中:

  • 你几乎不需要手动释放内存
  • 但这并不意味着内存是“无成本的”

真实工程中的问题往往表现为:

  • CPU 使用率异常升高
  • 延迟抖动(RT 不稳定)
  • 内存持续增长(看似“泄漏”)

工程认知:很多性能问题,本质是 GC 压力问题。


二、栈(stack)与堆(heap)的基本区别

栈的特点:

  • 函数调用自动分配
  • 分配和回收成本极低
  • 生命周期清晰(随函数结束)

堆的特点:

  • 跨函数、跨 goroutine 生命周期
  • 由 GC 管理
  • 分配和回收成本更高

核心目标:

让更多对象分配在栈上,而不是堆上。


三、逃逸分析(escape analysis)

Go 编译器会通过逃逸分析决定:

  • 一个对象是否可以安全地放在栈上
  • 还是必须逃逸到堆上

示例:

func foo() *int {
    x := 10
    return &x
}

说明:

  • x 的地址被返回
  • x 的生命周期超出函数
  • 必须分配到堆上

查看逃逸分析:

go build -gcflags="-m"

四、常见导致逃逸的场景

  • 返回局部变量指针
  • interface{} 承载具体值
  • 闭包引用外部变量
  • map / slice 存储指针或大对象

工程结论:逃逸并不一定是坏事,但大量逃逸会增加 GC 压力。


五、Go GC 的整体设计目标

Go 的 GC 目标不是“回收最快”,而是:

  • 低延迟(Low Latency)
  • 可预测(Predictable)
  • 并发执行(Concurrent)

这也是为什么 Go 选择:

  • 并发 GC
  • 三色标记算法

六、三色标记法(简化理解)

GC 过程中,对象被分为三类:

  • 白色:未访问,可能被回收
  • 灰色:已发现,但其引用尚未扫描
  • 黑色:已扫描,且其引用都已处理

GC 的目标是:

  • 从根对象出发
  • 标记所有可达对象为黑色
  • 剩余白色对象被回收

工程理解即可:GC 是“找还在用的”,不是“找不用的”。


七、并发 GC 与 STW(Stop-The-World)

Go GC 是并发的,但并非完全没有 STW。

GC 阶段简化为:

  • 短暂 STW(准备阶段)
  • 并发标记(与业务 goroutine 同时运行)
  • 短暂 STW(结束阶段)

工程结论:

  • 现代 Go 的 STW 非常短
  • 但频繁 GC 仍然会影响性能

八、GC 触发条件与 GOGC

GC 触发的核心条件:

  • 堆内存增长到一定比例

GOGC 控制 GC 频率:

export GOGC=100

含义:

  • 堆增长 100% 时触发 GC
  • 值越小,GC 越频繁
  • 值越大,占用内存越多

工程建议:

  • 默认值通常是合理的
  • 不要盲目调大或调小

九、常见 GC 压力来源

  • 大量短生命周期对象
  • 频繁分配大对象
  • 过多逃逸到堆
  • 不必要的 slice / map 扩容

工程方向:

  • 对象复用(sync.Pool)
  • 减少分配次数
  • 减少临时对象

十、面向 GC 的编程原则

  • 减少不必要的堆分配
  • 避免创建大量短命对象
  • 关注接口与逃逸
  • 不要过早优化,但要有意识

工程结论:性能优化的第一步,往往是减少分配。


十一、内存与 GC 总结

  • 栈分配几乎是“免费的”
  • 堆分配会带来 GC 成本
  • GC 不是问题,GC 压力才是问题

一句话总结:

写得越“省内存”,GC 越“省心”。


预告:Day 22|性能分析工具(pprof / trace)

下一天将系统讲解:

  • pprof 的使用方式
  • CPU / 内存分析
  • trace 的调度分析能力
  • 如何用工具而不是直觉做优化

发表评论