垃圾回收器(C 所沒有的替代方案)
/ GAR-bij /
在 C 中,你親手釋放每一塊區塊,而整個領域的錯誤——洩漏、懸置指標、重複釋放——都來自把那個親手的步驟做錯。許多其他語言完全去除了這項雜務:你配置記憶體、然後就只是不再使用它,語言便自動為你回收。做這件事的元件就是垃圾回收器。了解它之所以有用,正是因為 C 沒有它,這也是為什麼 C 對你要求如此多的小心。
核心概念:垃圾回收器週期性地找出你的程式仍能搆到哪些堆積物件,並回收所有搆不到的。經典方法是標記—清除(mark-and-sweep)。從根——你的程式能直接看到的指標,位於區域變數、全域變數與暫存器中——出發,回收器追蹤每一個指標,把它能搆到的每個物件標記為存活。任何它從未搆到的就是垃圾:你的程式再也無法引用它,所以回收它是安全的。回收器接著清掃所有未標記的物件,把它們的記憶體還回去供重用。由於可達性是自動計算的,你永遠不呼叫 free,而忘記釋放造成的洩漏與懸置指標這整類錯誤大致消失(不過在 GC 語言中,你仍可能因保留不再需要的引用而洩漏)。建立在這個概念上的語言包括 Java、Go、Python、JavaScript 與 C#。
它是一個真實的取捨,不是免費的午餐——這正是系統語言往往避開它的原因。回收器花費 CPU 時間去追蹤堆積、使用額外記憶體,且傳統上在運行時引入暫停,這對必須在嚴格期限內回應的程式碼可能無法接受。這是 C 與 Rust 刻意不用垃圾回收器、讓你精確控制記憶體何時釋放的部分原因。Rust 是有趣的中間路線:它沒有垃圾回收器,然而它的編譯器追蹤所有權與生命週期,自動插入釋放,並在編譯期證明沒有懸置指標或重複釋放——對這些錯誤有著 GC 般的安全,卻不需執行期的回收器,代價改為你在寫程式碼時所滿足的規則。
標記—清除,用淺白步驟說: 1. 從根(區域變數、全域變數、暫存器)出發。 2. 追蹤每個指標,把你能搆到的每個物件標記為存活。 3. 清除:回收每個從未被標記的物件。 C 沒有這樣的步驟——你必須自己呼叫 free()。
回收器自動回收搆不到的物件;C 把這件事完全留給你,這正是本領域諸多錯誤的根源。
垃圾回收器終結了手動釋放,卻並非沒有代價——它耗費 CPU 與記憶體、且可能讓程式暫停。它也無法防止邏輯層級的洩漏(保留你不再需要的引用)或死結。