Python 中的記憶體洩漏是什麼?原因、偵測方法、對策徹底解析

目次

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 視覺化參照結構

objgraphweakref等工具,可以圖形化視覺化物件之間如何相互參照。特別適合複雜的循環參照或意外殘留物件的調查。 透過組合以上這些工具與手法,可以有效特定記憶體洩漏發生的位置。
RUNTEQ(ランテック)|超実戦型エンジニア育成スクール

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 / ScaleneCPU・記憶體雙方的精確剖析對應大規模資料或 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. 重新驗證:確認修正後的效果

修正後,再次使用 tracemallocmemory_profiler,確認程式的記憶體消耗是否適當抑制。 實際上如果記憶體洩漏已解決,即使重複相同次數,記憶體的使用量也不會大幅增加。

一則提示:可視化工具的活用

此外,使用 objgraphmemory_profiler 可視化記憶體消耗的推移或參照關係,對追蹤更複雜的洩漏原因也很有幫助。 像這樣,「偵測→修正→重新驗證」的循環,實際動手體驗,是從根本解決記憶體洩漏問題的最短途徑。

8. 常見問題(FAQ)

本節將以問答形式彙整許多開發者對 Python 記憶體洩漏的疑問。從實際運營現場中常見的問題中挑選,並以易懂的方式說明。

Q1. Python 真的會發生記憶體洩漏嗎?

A. 是的,雖然 Python 擁有垃圾回收機制,但由於循環參照、外部函式庫的缺陷、長時間運作程序等情況,有時仍會發生記憶體洩漏。特別是在大量資料處理、C 擴展或第三方函式庫使用時,需要特別注意。

Q2. 如何辨識記憶體洩漏的徵兆?

A. 程式記憶體使用量持續逐漸增加、長期間運作後效能降低、發生強制結束或 OS 強制終止等是主要的徵兆。請定期使用pstop指令、監控工具等來觀測。

Q3. gc.collect()應該頻繁使用嗎?

A. 通常不需要,但如果記憶體消耗異常增加,或是大量使用循環參照時,適度使用是有效的。不過,過度使用會導致效能降低,因此只在必要時活用即可。

Q4. tracemalloc 和 memory_profiler 該用哪一個?

A. 根據目的來區分使用是最理想的。tracemalloc 適合調查「記憶體在哪裡增加」,memory_profiler 則適合函式單位或行單位的細微增減檢查。結合兩者使用會更有效。

Q5. 如果在外部函式庫中發現記憶體洩漏該怎麼辦?

A. 首先更新到最新版本,並確認官方資訊中是否有已知的 bug 或 Issue。如果仍無法解決,則考慮避免使用、尋找替代方案,或向開發者回報 bug 也是選擇之一。

Q6. 如果擔心記憶體洩漏,最初該做什麼?

A. 首先使用 tracemalloc 或 memory_profiler 等標準或主要工具,實際掌握記憶體在哪裡增加。如果原因難以特定,則將程式碼細分測試,逐步找出問題位置是基本做法。 參考本 FAQ,透過日常的記憶體管理及故障排除,即可實現更安全且高效的 Python 開發。

9. 總結

本篇文章以「Python 記憶體洩漏」為主題,從其基本機制、常見原因、偵測方法、因應・改善對策、便利工具,以及實踐範例和FAQ等,廣泛地進行了說明。 Python 是透過垃圾回收進行強力自動記憶體管理的語言,但由於循環參照、全域變數、外部函式庫的影響等,記憶體洩漏發生的風險絕非零。特別是在長時間運作的服務或處理大量資料的現場,早期發現與因應將直接連結到系統的穩定運作。 熟練運用能將「記憶體在何處、以何種程度被消耗」可視化的工具,當問題浮現時,分析原因並進行適當的修正・改善,這一點非常重要。tracemalloc、memory_profiler、gc、objgraph 等工具,將成為這第一步。 最後――記憶體洩漏是所有開發者身邊的課題。不要認為「我們沒問題」,定期監控與預防措施,將成為更舒適且安全的 Python 生活之鑰匙。