譯者 | 王德朕
策劃 | 云昭
內存管理對于編程的重要性不言而喻。不管是技術面試,還是實際生產環境,始終都是開發者繞不開的一個門檻。
在Java領域,“JVM調優”成為了一個熱議的話題。那么作為時不時占據編程排行榜的榜一大哥——Python,它是如何處理內存管理的呢?
本文就帶大家詳細了解Python垃圾回收系統的來龍去脈,以及如何避免它的陷阱。
Python為編程者提供了許多簡單上手的特性,其中最大的便利之一就是(幾乎)無障礙的內存管理。在Python中,不需要手動為Python中的對象和數據結構分配、跟蹤和釋放內存,Python運行時為你完成了所有這些工作,你可以專注于解決實際問題,而不是處理機器層面的細節。
盡管如此,對于經驗不足的Python程序員來說,了解Python的垃圾收集和內存管理是如何工作的還是很有好處的。理解這些機制將有助于避免復雜項目出現性能問題,還可以使用Python的內置工具來監視程序的內存管理行為。
在這篇文章中,我們將看看Python內存管理是如何工作的,它的垃圾回收系統如何幫助優化Python程序中的內存,以及如何使用標準庫和第三方模塊來控制內存使用和垃圾回收。
1、Python如何管理內存
每個Python對象都有一個引用計數,也被稱為refcount。refcount是對某個對象引用其它對象總數的統計。當你增加或刪除對一個對象的引用時,這個數字會上升或下降,當一個對象的refcount變為零時,該對象就會被刪除,其內存被釋放。
什么是引用?允許通過名稱或通過另一個對象中的訪問器訪問對象的任何東西。
這里有一個簡單的例子:
x = "Hello there"
運行這段 Python代碼,會發生兩件事:
1.字符串 “Hello there” 作為一個 Python 對象被創建并存儲在內存中;
2.在本地命名空間中創建變量 x ,并指向該對象,此時該對象的引用計數加1。
如果接下來的代碼是“y=x”,那么引用計數將再次提高到2。
每當x和y超出作用域或者從它們的名稱空間中刪除時,字符串x和y的引用計數都會減少1。一旦x和y都超出作用域或被刪除,字符串的refcount就變為0并被刪除。
什么是作用域
作用域是名稱空間的通用術語。默認情況下,在函數內定義的變量的作用域只是該函數,但在模塊級別定義的名稱的作用域是整個模塊。有關更多詳細信息,請參閱Python的文檔。
現在,假設我們創建一個包含字符串的列表,如下所示:
x = ["Hello there", 2, False]
字符串一直保留在內存中,直到列表本身被刪除或者包含字符串的元素從列表中被刪除。這兩個操作都會導致持有該字符串引用的對象消失。
現在考慮一下這個例子:
x="Hello there"
y=[x]
如果我們從y中刪除第一個元素,或者完全刪除列表y,那么字符串仍然在內存中,這是因為x包含對它的引用。
2、Python循環引用
大多數情況下,引用計數都是正常工作的,但有時你會遇到這樣的情況:兩個對象各自持有對方的一個引用,這就是所謂的循環引用。在這種情況下,對象的引用計數將永遠不會達到零,它們也永遠不會從內存中刪除。
這里有一個人為的例子:
x = SomeClass()
y = SomeOtherClass()
x.item = y
y.item = x
由于x和y保持對彼此的引用,即使沒有其它引用,它們也永遠不會從系統中刪除。
實際上,對于Python來說,為對象生成循環引用是相當常見的。一個例子是跟蹤對象的異常,該對象包含對異常本身的引用。
在Python的早期版本中,具有循環引用的對象可能會隨著時間積累,這對于長時間運行的應用程序來說是一個大問題。但Python后來引入了循環檢測和垃圾回收系統,用于管理循環引用。
3、Python垃圾回收器(GC)
Python的垃圾回收器檢測具有循環引用的對象。它通過跟蹤作為“容器”的對象--例如列表、字典、自定義類實例,并確定其中有哪些對象不被引用。
一旦這些對象被挑選出來,垃圾回收器就會通過把它們的引用計數降低到0來刪除它們。(有關這種方法的詳細信息,請參閱Python開發人員指南。)
絕大多數Python對象沒有循環引用,因此垃圾回收器不需要全天運行。相反,垃圾回收器使用一些方法來減少運行次數,并盡可能高效地運行。
當Python解釋器啟動時,它會跟蹤已分配但未釋放的對象數量,絕大多數Python對象的生命周期非常短,因此它們很快就會出現或消失。但是隨著時間的推移,長期存在的對象會逐漸積累,當這種對象的數量超過一定數量時,垃圾回收器就會運行。(在Python 3.10中,默認允許的長生命周期對象數是700。)
每次垃圾回收器運行時,它都會將收集后的所有對象放在一起,并將它們放在一個稱為“分代”的組中,在循環引用內,這些“第1代”對象被掃描的頻率較低。任何在垃圾回收器中幸存下來的第1代對象最終都會遷移到第2代,在第2代中,它們很少被掃描。
同樣,并不是所有的對象都會被垃圾回收器追蹤到,例如像用戶創建類這樣的復雜對象總是被跟蹤的,但是一個只保存簡單對象,例如整數和字符串的字典不會被跟蹤,因為在那個特定的字典中沒有對象持有對其的引用,不持有對其它元素的引用的簡單對象,如整數和字符串,永遠不會被跟蹤。
4、如何使用GC模塊
一般來說,垃圾回收器不需要調整就可以運行良好,Python的開發團隊選擇了常見情況的默認值,如果你確實需要調整垃圾回收的工作方式,你可以使用Python的GC?模塊,GC模塊為垃圾回收器的行為提供了編程接口,并可配置對哪些對象進行跟蹤。
GC讓你做的一件有用的事情是,當你確定不需要垃圾回收器的時候,可以關掉它。如果你有一個短期運行的腳本,堆積了大量的對象,你就不需要垃圾回收器。所有的東西都會在腳本結束時被清除掉。為此,你可以用gc.disable()命令禁用垃圾回收器,之后可以用gc.enable()重新啟用它。
你還可以使用gc.collect() 手動運行垃圾回收,這方面的一個常見應用是管理程序中生成許多臨時對象的部分,你可以在程序的這一部分禁用垃圾回收,然后在結束時手動運行回收并重新啟用回收。
另一個有用的垃圾回收優化是gc.free(),當運行該代碼后,垃圾回收器跟蹤的所有內容都被“凍結”,或者被列為免于回收掃描,這樣,未來的掃描可以跳過這些對象。如果你有一個導入庫并在啟動前設置大量內部狀態的程序,那么可以在完成所有工作之后發出gc.free()。這樣可以防止垃圾回收器搜尋那些無論如何都不可能被移除的東西。(如果希望將凍結的對象再次執行垃圾回收,請使用gc.unfree()。)
5、使用GC調試垃圾回收
還可以使用GC調試垃圾回收行為,如果內存中堆積的對象數量過多,而且沒有被垃圾回收,那么可以使用GC的檢查工具來確定哪些對象保存著對這些對象的引用。
如果想知道哪些對象保存著對給定對象的引用,可以使用gc.get_reference (obj)來列出它們,還可以使用gc.get_reference(obj)查找給定對象引用的任何。
如果不確定給定對象是否是垃圾回收的候選對象,gc.is_trace (obj)會告訴垃圾回收器是否跟蹤該對象,如前所述,請記住垃圾回收器不會跟蹤“原子”對象(如整數)或僅包含原子對象的元素。
如果希望親自查看正在收集的對象,可以使用gc.set_debug(gc.DEBUG _ LEAK | gc.DEBUG_STATS)設置垃圾收集器的調試標志。這將有關垃圾收集的信息寫入stderr,它將所有作為垃圾收集的對象保存在只讀列表gc.garbage中。
6、避免Python內存管理中的陷阱
如前所述,對象可能堆積在內存中,如果在某個地方仍然有對它們的引用,則不會被回收。這并不是Python的垃圾回收本身的問題,而是因為垃圾回收器無法判斷你是否意外地保留了對某些內容的引用。
讓我們以一些防止對象出現永不回收的提示來結束本文。
注意對象作用域
如果你把對象1指定為對象2的一個屬性(比如一個類),對象2需要在對象1之前退出作用范圍。
obj1 = MyClass()
obj2.prop = obj1
更重要的是,如果這是以其它操作的副作用形式方式發生的,例如把對象2作為參數傳遞給對象1的構造函數,你可能沒有意識到對象1有一個引用。
obj1 = MyClass(obj2)
另一個例子,如果你把一個對象推到一個模塊級的列表中,然后忘記了這個列表,這個對象將一直存在,直到從列表中刪除,或者直到列表本身不再有任何引用。但是如果這個列表是一個模塊級的對象,它很可能會一直存在,直到程序終止。
簡而言之,要意識到對象可能被另一個不明顯的對象引用。
使用weakref避免循環引用
Python的weakref模塊讓你創建對其它對象的弱引用,弱引用不會增加一個對象的引用數,所以一個只有弱引用的對象是垃圾回收的候選對象。
weakref的一個常見用途是對象的緩存,如果不希望被引用的對象僅僅因為有一個緩存條目而被保留下來,可以對緩存條目使用弱引用。
手動中斷循環引用
最后,如果你知道一個給定的對象持有對另一個對象的引用,你可以手動中斷對該對象的引用,如果你有instance_of_class.ref = other_object,當你準備移除instance_of_class時,你可以設置instance_of_class.ref = None。
原文鏈接:
??https://www.infoworld.com/article/3671673/python-garbage-collection-and-the-gc-module.html??
譯者介紹
王德朕,51CTO社區編輯,10年互聯網產研經驗,6年IT教培行業經驗。