GC 與 memory limit 的核心關係是:Go runtime 會根據 heap 成長決定何時執行 GC,而 memory limit 讓 runtime 有一個軟性記憶體目標。Memory limit 不是硬性上限,也不是 leak 修復工具;它是讓 runtime 更早回應記憶體壓力的控制訊號。

本章目標

學完本章後,你將能夠:

  1. 理解 heap growth、GOGC 與 GC 頻率的關係
  2. 判斷 debug.SetMemoryLimit 能解決什麼、不能解決什麼
  3. 從環境變數設定服務 memory limit
  4. 用 runtime metrics 觀察調整效果
  5. 分辨 GC 壓力、長期保留與真正 leak

【觀察】長時間服務的記憶體問題通常是趨勢問題

記憶體診斷的核心觀察是趨勢。Heap 是否持續上升、GC 後是否下降、goroutine 是否增加、某個操作後是否留下無法回收的資料,這些都比「現在用了多少 MB」更重要。

常見現象:

  • 啟動後 heap 穩定在某個區間:通常正常。
  • 每次高峰後 heap 都能下降:可能是短暫配置。
  • GC 後 heap 仍持續上升:可能有長期保留或 leak。
  • GC 次數快速增加且 CPU 升高:可能是 allocation 壓力。
  • goroutine 與 heap 同時增加:可能是 goroutine leak 或 send buffer 堆積。

Memory limit 可以幫 runtime 更積極控制 heap,但不能替代趨勢判讀。

【判讀】GC 控制的是 heap 成長

Go GC 的核心目標是回收不再被引用的 heap 物件。Runtime 會根據 GOGC 控制下一次 GC 觸發點。

1GOGC=100 go run ./cmd/server

GOGC=100 大致表示 heap 在上次 GC 後成長約 100% 時觸發下一次 GC。數字越小,GC 越頻繁,記憶體通常較低但 CPU 成本較高;數字越大,GC 較少,記憶體通常較高但 CPU 成本較低。

這是取捨,不是調大或調小就一定更好。CPU 緊繃的服務可能不能承受過低 GOGC;記憶體緊繃的服務可能不能承受過高 GOGC

【判讀】memory limit 是 runtime 軟目標

debug.SetMemoryLimit 的核心用途是告訴 Go runtime 希望整體記憶體使用量靠近某個目標。當 runtime 接近目標時,會更積極回收 heap。

1func configureRuntime() {
2    const limit = 512 << 20 // 512 MiB
3    debug.SetMemoryLimit(limit)
4}

這不是作業系統層級的硬限制。程式仍可能短暫超過這個值,特別是有大量非 Go heap 記憶體、cgo、mmap、大型 byte slice 尖峰或外部 library 配置時。

Memory limit 適合容器、桌面常駐服務、背景 worker、WebSocket server 這類需要避免吃掉過多資源的服務。若部署平台已有 memory limit,Go runtime 的 limit 通常應略低於平台限制,留給非 Go heap 與系統開銷。

【執行】設定值應來自部署環境

Memory limit 的核心配置原則是由部署環境決定,而不是寫死在 library 裡。應用入口可以讀取環境變數,解析後設定 runtime。

 1func ConfigureMemoryLimitFromEnv() error {
 2    raw := os.Getenv("APP_MEMORY_LIMIT_MB")
 3    if raw == "" {
 4        return nil
 5    }
 6
 7    mb, err := strconv.Atoi(raw)
 8    if err != nil {
 9        return fmt.Errorf("parse APP_MEMORY_LIMIT_MB: %w", err)
10    }
11
12    if mb <= 0 {
13        return fmt.Errorf("APP_MEMORY_LIMIT_MB must be positive")
14    }
15
16    debug.SetMemoryLimit(int64(mb) << 20)
17    return nil
18}

錯誤應在啟動時明確失敗。服務若用錯誤設定悄悄運行,後續記憶體行為會很難解釋。

【策略】runtime metrics 用來看調整是否有效

Runtime metrics 的核心用途是驗證調整效果。只改 GOGC 或 memory limit,不看 heap 與 GC 趨勢,容易變成憑感覺調參。

簡單方式可以用 runtime.ReadMemStats

1func ReadHeapAlloc() uint64 {
2    var stats runtime.MemStats
3    runtime.ReadMemStats(&stats)
4    return stats.HeapAlloc
5}

較完整的服務可以使用 runtime/metrics

1func ReadRuntimeSamples() []metrics.Sample {
2    samples := []metrics.Sample{
3        {Name: "/memory/classes/heap/objects:bytes"},
4        {Name: "/gc/cycles/total:gc-cycles"},
5        {Name: "/sched/goroutines:goroutines"},
6    }
7    metrics.Read(samples)
8    return samples
9}

觀察時要看趨勢:調整後 heap 峰值是否下降、GC 次數是否合理、CPU 是否上升、goroutine 是否仍持續增加。

【判讀】memory limit 不能修正仍被引用的資料

Memory limit 的核心邊界是它只能影響 GC 行為,不能讓仍被引用的物件消失。若程式把資料一直留在 map、slice、cache、goroutine 或 send buffer 裡,GC 不能回收。

1var cache = map[string][]byte{}
2
3func SavePayload(id string, payload []byte) {
4    cache[id] = payload
5}

如果 cache 沒有大小限制、TTL 或刪除策略,memory limit 只會讓 GC 更常跑,但資料仍被 cache 引用。真正修正是設計 cache 淘汰、分頁、快照大小限制或資料釋放路徑。

因此遇到 heap 持續上升時,下一步是用 pprof 確認誰保留了記憶體。

【策略】判斷是 GC 壓力還是長期保留

記憶體問題的核心分流是:物件被大量配置但很快回收,還是物件被長期保留。

現象可能問題下一步
alloc_space 高,inuse_space 不高短命配置多,GC 壓力大找熱路徑 allocation
inuse_space 持續上升長期保留或 leak看 heap profile retainers
goroutine 數量同步上升goroutine leak 或 queue 堆積看 goroutine profile
GC 次數暴增但 heap 仍高memory limit 壓力或資料保留檢查 cache/map/buffer

這個分流會決定後續工具。GC 參數能緩解壓力,但保留資料要回到資料結構與 lifecycle 修。

【測試】runtime 設定函式可以獨立測解析

Runtime 本身不需要在單元測試中反覆調參。應把環境解析邏輯獨立出來,測試輸入與錯誤即可。

 1func ParseMemoryLimitMB(raw string) (int64, error) {
 2    if raw == "" {
 3        return 0, nil
 4    }
 5    mb, err := strconv.Atoi(raw)
 6    if err != nil {
 7        return 0, fmt.Errorf("parse memory limit: %w", err)
 8    }
 9    if mb <= 0 {
10        return 0, fmt.Errorf("memory limit must be positive")
11    }
12    return int64(mb) << 20, nil
13}

測試:

1func TestParseMemoryLimitMB(t *testing.T) {
2    got, err := ParseMemoryLimitMB("512")
3    if err != nil {
4        t.Fatalf("parse memory limit: %v", err)
5    }
6    if got != 512<<20 {
7        t.Fatalf("limit = %d, want %d", got, int64(512<<20))
8    }
9}

這讓設定邏輯可測,而不需要在每個測試中真的改 runtime 狀態。

本章不處理

本章先處理單一 Go process 如何判讀 heap、GC 與 memory limit;平台 OOM 與部署合約,會在下列章節再往外延伸:

和 Go 教材的關係

這一章承接的是 runtime 壓力、allocation 與 pprof 診斷;如果你要先回看語言教材,可以讀:

小結

GC 控制 heap 回收節奏,memory limit 給 runtime 一個記憶體軟目標。合理設定能降低長時間服務的資源風險,但不能修正 cache、map、slice、goroutine 或 buffer 長期持有資料。診斷時先看趨勢,再用 pprof 區分 GC 壓力與長期保留。