目次
1. Python 也會發生記憶體洩漏──容易被忽略的陷阱
Python 常被認為「記憶體管理是自動的」,但實際上記憶體洩漏的風險並非零。特別是長時間運行的 Web 應用程式、機器學習・資料分析等大規模處理中,以不可見的形式持續消耗記憶體,最壞的情況下會導致系統當機或效能降低。 本篇文章將詳細說明 Python 中記憶體洩漏的本質、主要發生原因、偵測方法、具體對策,並穿插現場常用工具和範例程式碼。「Python 真的會發生記憶體洩漏嗎?」「程式長時間運行時動作變慢」「不知道該用什麼工具或步驟來調查」──對於有此類疑問或不安的人,本文旨在提供實踐性的解決方案。 首先,從 Python 的記憶體管理機制開始依序說明吧。2. Python 的記憶體管理機制
Python 具備「垃圾回收(GC)」這種自動記憶體管理機制。因此,不像 C 語言那樣需要程式設計師手動進行記憶體的確保或釋放。不過,並非一切都自動且完美無缺,記憶體洩漏仍有發生的可能性。在這裡,我們整理 Python 記憶體管理的基礎。透過參考計數的管理
Python 的物件是透過「參考計數」這種機制進行記憶體管理。這是內部計算某個物件被多少地方參考的機制。當參考計數變成 0 時,就會判斷該物件「已經沒有任何人使用」,並自動釋放記憶體。世代別垃圾回收(Generational GC)
然而,參考計數方式也有弱點。典型的例子是「循環參考」。例如,物件 A 參考物件 B,而 B 也參考 A,這樣雙方的參考計數就不會變成 0。為了因應這種情況,Python 內建了「世代別垃圾回收」。這是偵測僅靠參考計數無法回收的「孤立物件群」,如果不需要則一併釋放的機制。注意事項
Python 的自動記憶體管理非常方便,但並非對所有情況都萬能無敵。特別是外部函式庫的錯誤或大量使用 C 擴充模組時,可能會發生 Python 的 GC 無法因應的記憶體洩漏。此外,由於意外保留變數或大量使用全域變數,導致不必要的物件持續殘留在記憶體上的情況也存在,因此開發者有必要正確理解其機制。3. Python 中常見的記憶體洩漏原因模式
Python 的記憶體洩漏,主要原因是開發者在無意中讓「不必要的物件參照」持續殘留。在這裡,我們整理了在實際開發現場中經常見到的典型記憶體洩漏模式。循環參照導致的記憶體洩漏
Python 結合了參照計數和垃圾回收機制,但如果發生循環參照(物件彼此互相參照),垃圾回收可能無法正常運作。例如,在親子關係的類別中,「父類別參照子類別,子類別參照父類別」持續參照的情況就是典型案例。如果放任這種結構,不必要的物件就會持續殘留在記憶體中。全域變數・快取的過度保留
為了程式便利性,使用全域變數或快取(字典或清單等)是很常見的,但如果保留了超出必要的資料,就會導致無意的記憶體消耗。特別是,如果沒有明確刪除使用完畢的資料而任其放置,就會成為記憶體洩漏的原因。外部函式庫或 C 擴充模組導致的洩漏
Python 可以與許多外部函式庫或 C 擴充模組連携。然而,其中有些記憶體管理不完善,或是確保了垃圾回收目標外的記憶體區域。在這種情況下,即使 Python 端刪除了物件,實際的記憶體也不會被釋放。事件監聽器或回呼函式的參照殘留
在 GUI 應用程式或長時間運作的伺服器處理中,如果註冊了事件監聽器或回呼函式卻忘記解除,就會讓對應物件的參照殘留,持續消耗不必要的記憶體。其他典型範例
- 在大型清單或字典中累積過多臨時資料
- 閉包或 lambda 式中無意捕捉變數
- 類別實例持續將自身新增到清單或字典中
4. Python 中的記憶體洩漏偵測與剖析手法
要預防記憶體洩漏發生,重要的是要視覺化「目前使用了多少記憶體」以及「哪些物件持續增加」,並特定原因。Python 有各種利用標準功能或外部工具的偵測與剖析方法。在這裡,我們將介紹代表性的手法與工具。使用 tracemalloc 取得記憶體快照
Python 3.4 以降標準搭載的tracemalloc
模組,可以將程式執行時記憶體配置記錄為快照,並追蹤哪些處理消耗較多記憶體。
たとえば「どの関数で最もメモリが使われているか」「メモリの増加箇所のスタックトレース」を取得でき、メモリリーク調査の第一歩として非常に有効です。使用 memory_profiler 進行函式單位的記憶體消耗分析
memory_profiler
是一個外部程式庫,能詳細視覺化每個函式的記憶體使用量。腳本的每行記憶體消耗量可以透過圖表或文字確認,因此「特定處理中記憶體增加或減少多少」一目了然。pip install memory_profiler
即可輕鬆導入,從剖析結果中容易發現改善點是其特點。memray 或 Scalene 等進階剖析工具
如果想更詳細分析記憶體消耗、CPU 時間及堆積區域,則推薦使用「memray」或「Scalene」等剖析工具。 這些工具即使在龐大数据處理或包含 C 擴充的應用程式中,也能進行高精度的記憶體剖析。使用 gc 模組調查循環參照
活用標準程式庫gc
,可以偵測因循環參照而無法釋放的物件,或列出目前記憶體上殘留的物件。gc.collect()
可以強制執行垃圾回收,或使用gc.get_referrers()
追蹤參照來源,低階層調查也可能。使用 objgraph 或 weakref 視覺化參照結構
objgraph
或weakref
等工具,可以圖形化視覺化物件之間如何相互參照。特別適合複雜的循環參照或意外殘留物件的調查。 透過組合以上這些工具與手法,可以有效特定記憶體洩漏發生的位置。5. Python 中的記憶體洩漏的處理與改善方法
記憶體洩漏的原因特定後,下一步是進行適當的處理與改善,這非常重要。在 Python 中,主要有以下方法有效。明確的記憶體釋放:del 和 gc.collect() 的活用
透過明確刪除不再需要的物件參照,減少參照計數,並促使垃圾回收進行自動釋放。 例如,使用完大型清單或字典後,使用del
刪除變數,並視需要使用gc.collect()
立即回收不需要的物件。不過,在一般的 Python 程式中,不建議過度使用gc.collect()
。根據情況,如大量資料或長時間運行的程序,適當使用是重點。循環參照的解除與 weakref 的利用
如果懷疑有循環參照,則需要透過將不需要的參照設為None
等方式,明確斷開參照關係。
另外,如果結構無法避免循環參照,則可以利用weakref
(弱參照)模組,將其替換為「垃圾回收的目標參照」,以預防記憶體洩漏。
資源管理徹底使用 with 語句
檔案處理器、資料庫連線、socket 等資源,務必使用with
語法來管理。with
區塊離開時,資源會自動釋放,防止不需要的物件持續殘留在記憶體中。
例:with open("example.txt") as f:
data = f.read()
這樣寫可以防止忘記關閉檔案導致記憶體無法釋放等基本錯誤。注意外部函式庫與 C 擴充的記憶體管理
使用外部函式庫或 C 擴充模組時,更新至最新版本,或確認官方文件與 Issue 中是否有關記憶體管理的注意事項,這也很重要。視需要考慮替代函式庫,或透過ctypes
進行 C 語言端的明確記憶體釋放(例如呼叫malloc_trim
)也很有效。檢討快取與全域變數的管理
如果設計中大量使用快取或全域變數,則應徹底執行「使用完資料即刪除」「不囤積過多資料」等運作規則。有時引入限制快取大小的機制(LRU 快取等)會更安心。 透過意識這些要點,Python 應用程式的記憶體健全性將大幅提升。6. 主要記憶體解析工具的比較表
Python 的記憶體洩漏対策中,活用各種解析工具至關重要。然而,各工具皆有其特點與擅長領域。在此,將比較代表性的記憶體解析工具,並整理依用途推薦的重點。工具名 | 主要用途・特點 | 優點 |
---|---|---|
tracemalloc | 記憶體的快照比較・增加位置的特定 | 標準內建。能以函數・行單位追蹤記憶體的增減。 |
memory_profiler | 函數別的詳細記憶體消耗剖析 | 安裝簡單。行別的記憶體增減易於查看。 |
memray / Scalene | CPU・記憶體雙方的精確剖析 | 對應大規模資料或 C 擴充。能進行詳細的堆積分析。 |
gc 模組 | 循環參照或無法釋放物件的偵測 | 標準內建。能直接調查不必要物件。 |
objgraph / weakref | 參照關係的可視化・循環參照解決 | 將物件間的關係圖形化,能直觀地掌握。 |
依用途推薦場景
- 新手首先嘗試的話: tracemalloc・memory_profiler
- 追蹤複雜的循環參照: gc 模組+objgraph
- 需要外部 C 擴充或進階解析的情況: memray 或 Scalene
- 想查看參照結構時: objgraph/weakref
工具導入時的重點
- 標準內建工具的優點是能立即試用
- 外部工具可透過
pip install
導入,模仿官方文件範例是最快捷的方式 - 在本番環境測量負荷時,也需注意解析工具對動作的影響(overhead)
7. 以範例程式碼學習:偵測→修正→重新驗證的實踐流程
記憶體洩漏的對策不僅是理論,實際上「記憶體在哪裡增加」「應該如何修正」這些,要用自己的眼睛確認一邊進行是很重要的。在這裡,以典型的記憶體洩漏例子為基礎,用範例程式碼說明從偵測到修正・重新驗證的一連串流程。1. 使用 tracemalloc 偵測記憶體洩漏
例如,不斷向清單中添加不必要的物件的程式碼,是典型的記憶體洩漏模式。以下是簡單的例子。import tracemalloc
tracemalloc.start()
leak_list = []
for i in range(100000):
leak_list.append([0] * 1000) # 不斷添加不必要的巨大清單
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ 前 5 個記憶體消耗行 ]")
for stat in top_stats[:5]:
print(stat)
執行這個腳本,就可以透過 tracemalloc
掌握記憶體消耗集中在哪一行。2. 記憶體洩漏的修正範例
接著,修正不必要資料持續累積的原因。例如,「超過一定數量就清空清單」等工夫可以考慮。import tracemalloc
tracemalloc.start()
leak_list = []
for i in range(100000):
leak_list.append([0] * 1000)
if len(leak_list) > 1000:
leak_list.clear() # 定期清空清單來釋放
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ 前 5 個記憶體消耗行 ]")
for stat in top_stats[:5]:
print(stat)
像這樣,透過細心地刪除運作上不必要的資料,就可以抑制記憶體使用量的急增。3. 重新驗證:確認修正後的效果
修正後,再次使用tracemalloc
或 memory_profiler
,確認程式的記憶體消耗是否適當抑制。
實際上如果記憶體洩漏已解決,即使重複相同次數,記憶體的使用量也不會大幅增加。一則提示:可視化工具的活用
此外,使用objgraph
或 memory_profiler
可視化記憶體消耗的推移或參照關係,對追蹤更複雜的洩漏原因也很有幫助。 像這樣,「偵測→修正→重新驗證」的循環,實際動手體驗,是從根本解決記憶體洩漏問題的最短途徑。8. 常見問題(FAQ)
本節將以問答形式彙整許多開發者對 Python 記憶體洩漏的疑問。從實際運營現場中常見的問題中挑選,並以易懂的方式說明。Q1. Python 真的會發生記憶體洩漏嗎?
A. 是的,雖然 Python 擁有垃圾回收機制,但由於循環參照、外部函式庫的缺陷、長時間運作程序等情況,有時仍會發生記憶體洩漏。特別是在大量資料處理、C 擴展或第三方函式庫使用時,需要特別注意。Q2. 如何辨識記憶體洩漏的徵兆?
A. 程式記憶體使用量持續逐漸增加、長期間運作後效能降低、發生強制結束或 OS 強制終止等是主要的徵兆。請定期使用ps
或top
指令、監控工具等來觀測。