Python 進階主題與實務應用:記憶體管理與 GC(Garbage Collection)
簡介
在日常開發中,我們往往只關注「寫出功能正確的程式」,卻忽略了程式在執行期間如何使用與釋放記憶體。Python 內建的記憶體管理機制與自動垃圾回收(Garbage Collection, GC)雖然讓開發者免除手動釋放資源的負擔,但若不了解其運作原理,仍可能遭遇效能瓶頸、記憶體泄漏或不可預期的錯誤。
本篇文章將從 參考計數、循環引用、世代式 GC 三大核心概念切入,配合實用程式碼範例說明如何觀測、診斷與最佳化記憶體使用。文章適合有一定 Python 基礎的開發者,幫助你在大型專案或資源受限的環境中寫出更穩定、更高效的程式。
核心概念
1. 參考計數(Reference Counting)
Python 物件在建立時會被賦予一個 reference count,每當有新變數指向該物件,計數就會加一;變數離開作用域或被重新指派時,計數減一。當計數降到 0 時,物件立即被釋放。
import sys
a = [1, 2, 3] # 建立 list 物件,refcount 為 1
print(sys.getrefcount(a)) # 輸出 2,原因是 getrefcount 本身也暫時持有一個參考
b = a # 另一個變數指向同一個 list,refcount 變為 2
print(sys.getrefcount(a)) # 輸出 3
del b # 釋放 b,refcount 減 1
print(sys.getrefcount(a)) # 輸出 2
重點:
sys.getrefcount回傳的數值會比實際的計數多 1,因為它本身會暫時持有一個參考。
2. 循環引用(Cyclic References)
當兩個或以上的物件互相持有參考時,即使外部已無任何變數指向它們,reference count 仍不會降至 0,導致 記憶體泄漏。Python 透過 世代式垃圾回收(generational GC)來偵測並回收這類循環。
class Node:
def __init__(self, value):
self.value = value
self.next = None
# 建立兩個互相引用的 Node
n1 = Node(1)
n2 = Node(2)
n1.next = n2
n2.next = n1
# 解除外部變數的參考
del n1, n2
# 此時兩個 Node 仍互相引用,reference count > 0
若不啟用 GC,這段記憶體永遠不會被回收。Python 預設已開啟 GC,但了解何時手動觸發或暫停它,對於效能優化相當重要。
3. 世代式垃圾回收(Generational GC)
GC 依照物件的「存活時間」分成 三代(0、1、2 代)。新建立的物件先放入第 0 代,若在一次 GC 後仍存活,就晉升到第 1 代;同理,第 1 代存活的物件會被晉升到第 2 代。第 2 代的物件較少被檢查,因為大多數物件在短時間內就會死亡,這樣的設計能減少 GC 的開銷。
import gc
# 觀察目前 GC 的狀態
print("GC thresholds:", gc.get_threshold()) # (700, 10, 10) 為預設值
print("GC counts:", gc.get_count()) # (0, 0, 0) 為目前各代的收集次數
# 手動觸發一次完整的垃圾回收(包括所有世代)
gc.collect()
3.1 調整 GC 參數
在記憶體受限或高併發的情境下,你可以自行調整 threshold(觸發收集的計數):
# 將第 0 代的觸發門檻降低,讓 GC 更頻繁執行
gc.set_threshold(300, 10, 10)
print("New thresholds:", gc.get_threshold())
注意:過低的門檻會導致 GC 過度頻繁,反而拖慢程式;過高則可能讓循環引用長時間佔用記憶體。
4. __del__ 與資源釋放
在 C 語言中,我們習慣使用 free 手動釋放記憶體;在 Python,若物件需要釋放外部資源(檔案、網路連線、資料庫連結),應該實作 context manager(with 語句)或 __del__ 方法。
class FileWriter:
def __init__(self, path):
self.file = open(path, "w")
def write(self, text):
self.file.write(text)
def __del__(self):
# 當物件被 GC 回收時,自動關閉檔案
self.file.close()
print("File closed by __del__")
# 使用方式
writer = FileWriter("demo.txt")
writer.write("Hello, GC!")
del writer # 立即觸發 __del__,關閉檔案
最佳實踐是使用 with 取代 __del__,因為 __del__ 的執行時機不一定(尤其在循環引用中會被延遲):
with open("demo.txt", "w") as f:
f.write("Hello, with!")
# 離開 with 區塊即自動關閉檔案
程式碼範例(實用示例)
範例 1:觀測記憶體使用量
import tracemalloc
tracemalloc.start()
def generate_data():
return [i for i in range(10_000)]
data = generate_data()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 5 memory blocks ]")
for stat in top_stats[:5]:
print(stat)
使用
tracemalloc可以快速定位哪一段程式碼佔用了大量記憶體,對除錯非常有幫助。
範例 2:手動觸發 GC 以釋放大型循環結構
import gc
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
def build_cyclic_tree(depth):
root = TreeNode(0)
cur = root
for i in range(1, depth):
child = TreeNode(i)
cur.children.append(child)
child.children.append(cur) # 故意形成循環
cur = child
return root
root = build_cyclic_tree(1000)
del root # 解除外部參考
collected = gc.collect()
print(f"GC 回收了 {collected} 個不可達物件")
範例 3:使用 weakref 打破循環引用
import weakref
class Parent:
def __init__(self, name):
self.name = name
self.child = None
class Child:
def __init__(self, parent):
# 使用 weakref.ref,避免子物件持有強參考
self.parent = weakref.ref(parent)
p = Parent("Alice")
c = Child(p)
p.child = c
del p, c
print("完成,GC 會自動回收")
weakref讓你可以保留「弱參考」而不影響 reference count,常用於緩存或觀察者模式。
範例 4:自訂 GC 回呼(debug)
import gc
def gc_callback(phase, info):
print(f"GC {phase}:{info}")
gc.callbacks.append(gc_callback)
# 產生大量物件,觸發 GC
objs = [object() for _ in range(2000)]
del objs
gc.collect() # 會呼叫上面的回呼
範例 5:結合 contextmanager 與資源清理
from contextlib import contextmanager
@contextmanager
def managed_list():
lst = []
try:
yield lst
finally:
# 清理工作,例如寫入日誌或釋放外部資源
print("Managed list is being cleaned up")
with managed_list() as mylist:
mylist.extend(range(100))
print("使用中:", len(mylist))
# 離開區塊自動執行 finally
常見陷阱與最佳實踐
| 常見陷阱 | 可能後果 | 解決方案 |
|---|---|---|
| 忘記關閉檔案或網路連線 | 記憶體與系統資源泄漏,甚至檔案鎖死 | 使用 with 句法或實作 __enter__/__exit__ |
| 大量小物件造成頻繁 GC | 程式效能下降 | 盡量重用物件、調整 GC thresholds、使用 list 代替多次 append |
循環引用搭配 __del__ |
__del__ 可能不會被呼叫,資源無法釋放 |
避免在有循環的類別中實作 __del__,改用 weakref 或 context manager |
過度依賴 gc.collect() |
強制 GC 會暫停執行緒,影響即時性 | 僅在確定記憶體壓力極高時使用,平時讓 Python 自動管理 |
忽視 tracemalloc 與 objgraph |
難以定位記憶體泄漏根源 | 在開發或測試階段加入記憶體分析工具,定期檢查圖形化引用圖 |
最佳實踐摘要:
- 以
with取代手動close:保證資源即時釋放。 - 使用
weakref打破不必要的循環,特別是觀察者或緩存結構。 - 定期檢測記憶體:
tracemalloc、objgraph、psutil能快速找出異常。 - 調整 GC thresholds:根據服務的負載與記憶體上限微調。
- 保持物件生命週期簡潔:避免長時間持有全域參考或大型容器。
實際應用場景
1. 大數據處理(Pandas / NumPy)
在處理百萬筆資料時,DataFrame 佔用的記憶體往往超過系統可用空間。透過 分批讀取、釋放不再使用的中間變數(del + gc.collect()),以及 使用 astype 轉換為更小的 dtype,可以大幅降低峰值記憶體。
import pandas as pd, gc
chunks = pd.read_csv("big.csv", chunksize=500_000)
for i, chunk in enumerate(chunks):
# 只保留需要的欄位與類型
df = chunk[['id', 'value']].astype({'id': 'int32', 'value': 'float32'})
# 處理...
del df
gc.collect() # 確保前一次 chunk 釋放
2. Web 框架(Django / Flask)長程服務
Web 伺服器會持續接受請求,若每個請求產生大量臨時物件且未正確釋放,會造成 記憶體漸增(memory leak)。常見做法:
- 使用
@contextmanager包裝資料庫交易,使連線在請求結束時自動關閉。 - 在中介層(middleware)加入
gc.collect(),僅在偵測到記憶體使用率超過閾值時執行。 - 對於大型快取(如
lru_cache),設定maxsize,避免無限制增長。
3. 機器學習模型部署
模型權重(TensorFlow / PyTorch)往往佔用數百 MB。部署時常見的技巧:
- 載入模型後立即呼叫
torch.cuda.empty_cache()(GPU)或gc.collect()(CPU)釋放暫存。 - 使用
weakref.WeakValueDictionary存放已載入的模型,讓未被使用的模型自動回收。
import torch, weakref
model_cache = weakref.WeakValueDictionary()
def get_model(name):
if name in model_cache:
return model_cache[name]
model = torch.load(f"{name}.pt")
model_cache[name] = model
return model
總結
- Python 的記憶體管理 以參考計數為基礎,搭配世代式垃圾回收解決循環引用問題。
- 了解 GC 的門檻、世代與手動觸發,能在記憶體緊張的情況下做出即時調整。
weakref、contextmanager、tracemalloc等工具是開發者防止記憶體泄漏與提升效能的好幫手。- 在 大數據、Web 服務與機器學習 等實務場景中,適當的記憶體觀測與釋放策略是保持系統穩定與成本可控的關鍵。
掌握了上述概念與技巧後,你將能在開發 Python 應用時更自信地管理記憶體,避免隱蔽的效能瓶頸,寫出既 易讀 又 高效 的程式碼。祝你在實務專案中玩得開心、寫得順利!