本文 AI 產出,尚未審核

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 managerwith 語句)或 __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 自動管理
忽視 tracemallocobjgraph 難以定位記憶體泄漏根源 在開發或測試階段加入記憶體分析工具,定期檢查圖形化引用圖

最佳實踐摘要

  1. with 取代手動 close:保證資源即時釋放。
  2. 使用 weakref 打破不必要的循環,特別是觀察者或緩存結構。
  3. 定期檢測記憶體tracemallocobjgraphpsutil 能快速找出異常。
  4. 調整 GC thresholds:根據服務的負載與記憶體上限微調。
  5. 保持物件生命週期簡潔:避免長時間持有全域參考或大型容器。

實際應用場景

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 的門檻、世代與手動觸發,能在記憶體緊張的情況下做出即時調整。
  • weakrefcontextmanagertracemalloc 等工具是開發者防止記憶體泄漏與提升效能的好幫手。
  • 大數據、Web 服務與機器學習 等實務場景中,適當的記憶體觀測與釋放策略是保持系統穩定與成本可控的關鍵。

掌握了上述概念與技巧後,你將能在開發 Python 應用時更自信地管理記憶體,避免隱蔽的效能瓶頸,寫出既 易讀高效 的程式碼。祝你在實務專案中玩得開心、寫得順利!