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/6785

(0)
Walker的頭像Walker
上一篇 10小時前
下一篇 13小時前

相關推薦

  • 編程基礎 0004_Web_beego開發

    beego 開始 2 文章的添加與刪除 創建 TopicController // controllers中添加topic.go package controllers import "github.com/astaxie/beego" type TopicController struct { beego.Controller } fu…

    後端開發 14小時前
    100
  • 編程基礎 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…

    後端開發 18小時前
    000
  • Go工程師體系課 006

    項目結構說明:user-web 模塊 user-web 是 joyshop_api 工程中的用戶服務 Web 層模塊,負責處理用戶相關的 HTTP 請求、參數校驗、業務路由以及調用後端接口等功能。以下是目錄結構說明: user-web/ ├── api/ # 控制器層,定義業務接口處理邏輯 ├── config/ # 配置模塊,包含系統配置結構體及讀取邏輯 …

  • Go日積月累 go-s3-upload-example

    Go 語言實現文件上傳到 AWS S3 示例 本示例演示如何使用 Go 和 AWS SDK v2 將本地文件上傳到 Amazon S3。 🧾 前提條件 已擁有 AWS 賬號; 已創建 S3 Bucket; 已配置 AWS 憑證(通過 aws configure 或設置環境變量); 已準備本地文件(如 test.jpg); 📦 安裝依賴 go mod init…

  • 編程基礎 0013_Go企業實踐案例精華

    Go 企業實踐案例精華 知識來源:基於以下電子書資料整理- 《Go在百度BFE的應用 for Gopher China》- 《Go在分布式數據庫中的應用》- 《Go在獵豹移動的應用》- 《Golang與高性能DSP競價系統》- 《Go at Google: Language Design in the Service of Software Engineer…

    後端開發 18小時前
    000
簡體中文 繁體中文 English