C 程式設計師的噩夢是這樣的:
char *ptr = malloc(100);
// ... 用了 ptr
free(ptr);
// ... 後來又不小心用了 ptr
*ptr = 'x'; // use-after-free,undefined behaviorfree 之後繼續用這塊記憶體,行為是 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 不是免費的,它帶來幾個特性:
-
Latency jitter:GC 運行時會消耗 CPU,產生無法預測的暫停(stop-the-world 或 concurrent 但有競爭)。高頻交易、即時遊戲——這些場景對 GC pause 非常敏感。
-
記憶體使用率比較高:GC 需要一定的「空間」來決定什麼是垃圾。Go 的 GC 在記憶體使用率接近 100% 時效率最低;一般建議 live heap 不超過可用記憶體的 50-70%。
-
你不能控制回收時機:你知道你不需要這個物件了,但 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,因為開發速度的代價太高。