Go工程师体系课 019

Go 内存模型与 GC

1. 内存分配基础

1.1 栈(Stack)与堆(Heap)

┌─────────────────────────────┐
│          堆 (Heap)           │  ← 动态分配,GC 管理
│   ┌─────┐ ┌─────┐ ┌─────┐  │
│   │ obj │ │ obj │ │ obj │  │
│   └─────┘ └─────┘ └─────┘  │
├─────────────────────────────┤
│       goroutine 栈 1        │  ← 2KB 起始,自动增长
│   ┌─────────────────────┐   │
│   │ 局部变量、函数参数   │   │
│   └─────────────────────┘   │
├─────────────────────────────┤
│       goroutine 栈 2        │
│   ┌─────────────────────┐   │
│   │ 局部变量、函数参数   │   │
│   └─────────────────────┘   │
└─────────────────────────────┘
  • 栈分配:速度极快(只需移动栈指针),函数返回自动回收
  • 堆分配:需要 GC 回收,有额外开销
  • Go 编译器通过逃逸分析决定变量分配在栈还是堆

1.2 逃逸分析(Escape Analysis)

# 查看逃逸分析结果
go build -gcflags="-m" main.go
go build -gcflags="-m -m" main.go  # 更详细
// 不逃逸:局部变量,分配在栈上
func noEscape() int {
    x := 42
    return x // 值拷贝,x 不逃逸
}

// 逃逸:返回指针,分配在堆上
func escape() *int {
    x := 42
    return &x // x 逃逸到堆,因为函数返回后还要用
}
// go build -gcflags="-m" 输出: moved to heap: x

// 逃逸:接口类型参数
func printVal(v interface{}) {
    fmt.Println(v) // v 逃逸(interface{} 导致)
}

// 逃逸:闭包引用
func closure() func() int {
    x := 0
    return func() int {
        x++ // x 被闭包捕获,逃逸到堆
        return x
    }
}

// 逃逸:发送到 channel
func chanEscape(ch chan *int) {
    x := 42
    ch <- &x // x 逃逸
}

// 不逃逸:已知大小的切片
func noEscapeSlice() {
    s := make([]int, 10)    // 不逃逸(编译期已知大小)
    _ = s
}

// 逃逸:运行时大小的切片
func escapeSlice(n int) {
    s := make([]int, n)     // 逃逸(编译期不知道大小)
    _ = s
}

1.3 变量何时分配到堆上

场景 是否逃逸
返回局部变量的指针 逃逸
赋值给 interface{} 逃逸
闭包捕获局部变量 逃逸
发送指针到 channel 逃逸
slice/map 运行时大小 可能逃逸
超过一定大小的对象 逃逸
纯值类型局部变量 不逃逸
编译期已知大小的 slice 不逃逸

2. Go 内存分配器

Go 的内存分配器基于 tcmalloc(Thread-Caching Malloc)思想。

2.1 三级结构

  goroutine ──► mcache (每个P一个,无锁)
                   │
                   ▼
               mcentral (全局,有锁,按 size class)
                   │
                   ▼
                mheap (全局,向 OS 申请)
                   │
                   ▼
              操作系统 (mmap/sbrk)
  • mcache:每个 P 拥有一个,分配小对象时无需加锁
  • mcentral:当 mcache 用完时,从 mcentral 获取,需要加锁
  • mheap:当 mcentral 也不够时,从 mheap 申请,mheap 向 OS 要内存

2.2 mspan 与 size class

Go 将对象按大小分为约 70 个 size class(8B, 16B, 32B, 48B, ...32KB)。

  • 微小对象(<16B):使用 tiny allocator,多个小对象可共享一个内存块
  • 小对象(16B-32KB):按 size class 从 mcache 分配
  • 大对象(>32KB):直接从 mheap 分配
// 观察分配行为
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("堆分配次数: %d\n", m.Mallocs)
fmt.Printf("堆释放次数: %d\n", m.Frees)
fmt.Printf("堆使用字节: %d\n", m.HeapAlloc)
fmt.Printf("系统内存:    %d\n", m.Sys)

3. 垃圾回收(GC)

3.1 三色标记法

Go 使用并发三色标记-清除算法。

白色(White): 未被标记,GC 结束后回收
灰色(Gray):  已标记但子对象未扫描
黑色(Black): 已标记且子对象已扫描,保留

标记过程:
1. 所有对象初始为白色
2. 从根对象(栈、全局变量)出发,标记为灰色
3. 取出一个灰色对象,扫描它引用的所有对象:
   - 白色 → 标记为灰色
   - 已经是灰色或黑色 → 跳过
   - 当前对象标记为黑色
4. 重复步骤3,直到没有灰色对象
5. 剩余的白色对象就是垃圾,回收
初始:    [白A] → [白B] → [白C]
                        ↗
         [白D] → [白E]

Step1:   [灰A] → [白B] → [白C]    (A 是根对象)
                        ↗
         [灰D] → [白E]             (D 是根对象)

Step2:   [黑A] → [灰B] → [白C]    (扫描A,B变灰)
                        ↗
         [黑D] → [灰E]             (扫描D,E变灰)

Step3:   [黑A] → [黑B] → [灰C]    (扫描B,C变灰)
                        ↗
         [黑D] → [黑E]             (扫描E,C已灰)

Step4:   [黑A] → [黑B] → [黑C]    (扫描C,完成)
         [黑D] → [黑E]

→ 没有白色对象,全部保留

3.2 写屏障(Write Barrier)

GC 与用户代码并发执行时,用户可能修改引用关系。写屏障确保不会遗漏存活对象。

Go 1.8+ 使用混合写屏障(Hybrid Write Barrier):
- 标记开始时,栈上的对象全部标为黑色
- 堆上新创建的对象标为黑色
- 被覆盖的指针指向的对象标为灰色

3.3 GC 触发条件

// 1. 堆内存增长到上次 GC 后的 GOGC% 时触发(默认 100%,即翻倍)
// 环境变量控制
GOGC=100  // 默认,堆增长 100% 时触发
GOGC=200  // 堆增长 200% 时触发(GC 频率降低,内存占用增加)
GOGC=50   // 堆增长 50% 时触发(GC 更频繁,内存占用降低)
GOGC=off  // 关闭 GC

// 2. 定时触发:距离上次 GC 超过 2 分钟
// 3. 手动触发
runtime.GC()

3.4 GOMEMLIMIT(Go 1.19+)

软内存上限,当接近上限时 GC 会更积极地回收。

// 环境变量
GOMEMLIMIT=1GiB   // 软限制 1GB
GOMEMLIMIT=512MiB

// 代码中设置
import "runtime/debug"
debug.SetMemoryLimit(1 << 30) // 1GB

// 典型用法:容器环境
// 容器限制 2GB,设置 GOMEMLIMIT=1.5GiB
// 留 0.5GB 给非堆内存(栈、goroutine等)

3.5 GC 阶段

  用户代码运行  │ STW │  并发标记  │ STW │ 并发清除 │ 用户代码运行
                │Mark │           │Mark │         │
                │Setup│           │Term │         │
                │     │           │     │         │
               <1ms              <1ms
  • Mark Setup (STW):开启写屏障,通常 <1ms
  • 并发标记:与用户代码并行运行
  • Mark Termination (STW):关闭写屏障,通常 <1ms
  • 并发清除:回收白色对象

4. 内存泄漏常见场景

4.1 goroutine 泄漏

// 泄漏:goroutine 永远阻塞在 channel
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch // 永远收不到数据,goroutine 泄漏
        fmt.Println(val)
    }()
    // 函数返回,ch 没人发数据,goroutine 永远不会结束
}

// 修复:使用 context 取消
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return // 可以退出
        }
    }()
}

4.2 time.After 在循环中使用

// 泄漏:每次循环创建一个 Timer,直到触发才会被 GC
func leak() {
    ch := make(chan int)
    for {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-time.After(5 * time.Second): // 每次循环都新建 Timer
            fmt.Println("timeout")
        }
    }
}

// 修复:使用 time.NewTimer 并手动 Reset
func noLeak() {
    ch := make(chan int)
    timer := time.NewTimer(5 * time.Second)
    defer timer.Stop()
    for {
        timer.Reset(5 * time.Second)
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-timer.C:
            fmt.Println("timeout")
        }
    }
}

4.3 slice 底层数组未释放

// 泄漏:切片引用大数组的一小部分
func leak() []byte {
    data := make([]byte, 1<<20) // 1MB
    // ... 填充数据
    return data[:10] // 返回 10 字节,但底层 1MB 数组不会被回收
}

// 修复:拷贝需要的数据
func noLeak() []byte {
    data := make([]byte, 1<<20)
    result := make([]byte, 10)
    copy(result, data[:10])
    return result
}

4.4 全局变量持有引用

// 泄漏:全局 map 不断增长
var cache = make(map[string][]byte)

func processRequest(key string, data []byte) {
    cache[key] = data // 只增不删,内存不断增长
}

// 修复:设置过期策略或大小限制
// 使用 LRU cache 或定期清理

5. runtime 包常用函数

package main

import (
    "fmt"
    "runtime"
    "runtime/debug"
)

func main() {
    // CPU 和 Goroutine 信息
    fmt.Println("CPU 核数:", runtime.NumCPU())
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
    fmt.Println("goroutine 数:", runtime.NumGoroutine())
    fmt.Println("Go 版本:", runtime.Version())

    // 内存统计
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("堆分配: %d MB\n", m.HeapAlloc/1024/1024)
    fmt.Printf("系统内存: %d MB\n", m.Sys/1024/1024)
    fmt.Printf("GC 次数: %d\n", m.NumGC)
    fmt.Printf("上次 GC 暂停: %d μs\n", m.PauseNs[(m.NumGC+255)%256]/1000)

    // 手动触发 GC
    runtime.GC()

    // 设置 GC 百分比(等价于 GOGC 环境变量)
    debug.SetGCPercent(200) // 堆增长 200% 时触发 GC

    // 设置内存限制(Go 1.19+)
    debug.SetMemoryLimit(512 << 20) // 512MB

    // 释放未使用的内存给 OS
    debug.FreeOSMemory()

    // SetFinalizer: 对象被 GC 回收前执行清理
    // 注意:不保证一定执行,不要用于关键清理
    type Resource struct{ Name string }
    r := &Resource{"test"}
    runtime.SetFinalizer(r, func(r *Resource) {
        fmt.Println("清理资源:", r.Name)
    })
}

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6766

(0)
Walker的头像Walker
上一篇 3小时前
下一篇 5小时前

相关推荐

  • 编程基础 0006_并发进阶_sync包与Context

    并发进阶:sync 包与 Context 一、sync 包详解 1. sync.Mutex 与 sync.RWMutex // Mutex: 互斥锁,同一时间只有一个 goroutine 能持有 var mu sync.Mutex var count int func increment() { mu.Lock() defer mu.Unlock() cou…

  • Go工程师体系课 017

    限流、熔断与降级入门(含 Sentinel 实战) 结合课件第 3 章(3-1 ~ 3-9)的视频要点,整理一套面向初学者的服务保护指南,帮助理解“为什么需要限流、熔断和降级”,以及如何用 Sentinel 快速上手。 学习路线速览 3-1 理解服务雪崩与限流、熔断、降级的背景 3-2 Sentinel 与 Hystrix 对比,明确技术选型 3-3 Sen…

    后端开发 1天前
    1000
  • Go工程师体系课 007

    商品微服务 实体结构说明 本模块包含以下核心实体: 商品(Goods) 商品分类(Category) 品牌(Brands) 轮播图(Banner) 品牌分类(GoodsCategoryBrand) 1. 商品(Goods) 描述平台中实际展示和销售的商品信息。 字段说明 字段名 类型 说明 name String 商品名称,必填 brand Pointer …

  • Go工程师体系课 protoc-gen-validate

    protoc-gen-validate 简介与使用指南 ✅ 什么是 protoc-gen-validate protoc-gen-validate(简称 PGV)是一个 Protocol Buffers 插件,用于在生成的 Go 代码中添加结构体字段的验证逻辑。 它通过在 .proto 文件中添加 validate 规则,自动为每个字段生成验证代码,避免你手…

  • 编程基础 0012_Go_Web与网络编程精华

    Go Web 与网络编程精华 知识来源:- 《Building Web Apps with Go》- 《Go API 编程》- 《Go Web 编程》(Go Web Programming, Sau Sheong Chang)- 《Go 网络编程》(Network Programming with Go)- 《Mastering Go Web Service…

简体中文 繁体中文 English