C 程式設計師的噩夢是這樣的:

char *ptr = malloc(100);
// ... 用了 ptr
free(ptr);
// ... 後來又不小心用了 ptr
*ptr = 'x';  // use-after-free,undefined behavior

free 之後繼續用這塊記憶體,行為是 undefined——可能沒事,可能 crash,可能讓攻擊者注入任意程式碼。Heartbleed 是 buffer over-read,WannaCry 利用的 EternalBlue 是 buffer overflow——這類 C/C++ 記憶體錯誤(use-after-free、buffer overflow、null pointer dereference)是大量嚴重安全漏洞的根源。

Python、Java、Go 的工程師很少碰到這類問題,因為他們不自己管記憶體。但這不是免費的——你用 GC 換走了記憶體安全的心智負擔,換來的是你不完全控制記憶體的回收時機。


三種哲學

1. Garbage Collection(GC)

讓 runtime 追蹤所有記憶體的引用,沒有引用的物件自動回收。

Python、Java、Go、JavaScript、Ruby、C#——主流後端語言大多選這條路。

Tracing GC(Go、Java)

定期從 root(全域變數、stack 上的指標)開始追蹤,找到所有可達的物件,剩下的就是垃圾,回收。

Go 的 GC 使用 tri-color marking:

  • 白色:還沒確認可達
  • 灰色:可達,但還沒掃它的子物件
  • 黑色:可達,子物件也掃完了

GC 運行時不需要暫停整個程式(concurrent marking),Go 1.5 之後把 stop-the-world 時間壓到 1ms 以下。

Reference Counting(Python、Swift)

每個物件記著它被引用了幾次。引用數降到 0,立刻回收。

a = [1, 2, 3]  # reference count = 1
b = a           # reference count = 2
b = None        # reference count = 1
a = None        # reference count = 0 → 立刻回收

優點:物件一沒用就立刻釋放,不需要等 GC 掃描。
弱點:循環引用——A 引用 B,B 引用 A,兩個的引用數都不會到 0,永遠不會被回收(Python 有額外的 cycle detector 補救)。


GC 的代價

GC 不是免費的,它帶來幾個特性:

  1. Latency jitter:GC 運行時會消耗 CPU,產生無法預測的暫停(stop-the-world 或 concurrent 但有競爭)。高頻交易、即時遊戲——這些場景對 GC pause 非常敏感。

  2. 記憶體使用率比較高:GC 需要一定的「空間」來決定什麼是垃圾。Go 的 GC 在記憶體使用率接近 100% 時效率最低;一般建議 live heap 不超過可用記憶體的 50-70%。

  3. 你不能控制回收時機:你知道你不需要這個物件了,但 GC 可能過一段時間才回收它。在資源密集的場景(大量打開 file handle、持有 TCP 連線),這可能造成問題——你要自己記得 Close() 或用 defer


2. Ownership(Rust)

Rust 的記憶體管理在編譯時完成,沒有 GC,沒有 runtime overhead。

核心規則:

  • 每個值有一個 owner
  • Owner 離開 scope,值被釋放
  • 你可以把 ownership 轉移(move)或借用(borrow)
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的 ownership 轉移給 s2
    // println!("{}", s1);  // 編譯錯誤:s1 已經被 move,不能再用
    println!("{}", s2);  // OK
}  // s2 離開 scope,"hello" 被釋放

這個設計讓 Rust 在編譯時就能保證:

  • 沒有 use-after-free
  • 沒有 double-free
  • 沒有 null pointer dereference
  • 沒有 data race(借用規則確保同一時間只有一個可變引用)

代價是學習曲線——初學者花在跟 borrow checker 搏鬥的時間,比學其他語言的整個語法還長。

為什麼後端工程師應該知道 Ownership,即使不寫 Rust

Ownership 是一種思考記憶體生命週期的方式,在任何語言都有用。

在 Go 裡,當你在 goroutine 之間共享一個 slice,你要問:誰擁有這個 slice?誰負責它的生命週期?誰能修改它?這些問題在 Rust 裡是編譯器強迫你回答的,在 Go 裡是你自己要想清楚的。


3. 手動管理(C/C++)

你自己 malloc、自己 free、自己保證不出錯。

完全的控制,完全的責任。記憶體分配/釋放的時機完全確定,效能極度可預測,但任何錯誤都是 undefined behavior。

現代後端幾乎沒有人在寫 C 的業務邏輯,但你依賴的基礎設施——nginx、Redis、PostgreSQL——是 C 寫的。理解手動記憶體管理,讓你在看這些工具的原始碼或 issue 時不會完全不知道在說什麼。


後端開發的實際影響

GC pause 和 API latency

如果你的服務有嚴格的 p99 latency 要求(比如 100ms 以內),GC pause 是一個真實的威脅。Go 的 GC 通常 < 1ms,但在 heap 很大(幾十 GB)或分配率很高的場景,pause 可以到幾十 ms。

處理方式:

  • 減少 allocation(複用物件、用 sync.Pool
  • 讓 live heap 有足夠的 headroom
  • GOGC/GOMEMLIMIT 調整 GC 觸發閾值

Python 的記憶體管理雙層結構

Python 有 reference counting(立刻回收),也有 cycle detector(循環引用)。但 Python 的記憶體不一定立刻還給 OS——Python runtime 有自己的 memory pool,即使你刪掉很多物件,從 OS 看到的 RSS 可能還是很高。

這讓 Python 的記憶體分析比 Go/Java 的 heap profiling 更複雜。

Rust 在後端的利基

記憶體安全 + 無 GC pause——這讓 Rust 在幾個場景有優勢:

  • 需要穩定低 latency(不容 GC pause)的 API path
  • 需要 C 互通(FFI)的場景
  • 系統工具(CLI、daemon、proxy)

業務邏輯層通常不值得換 Rust,因為開發速度的代價太高。

下一篇:Heap vs Stack 跨語言