Python 為開發(fā)者提供了許多便利,其中最大的便利之一是其幾乎無憂的內(nèi)存管理。開發(fā)者無需手動為 Python 中的對象和數(shù)據(jù)結(jié)構(gòu)分配、跟蹤和釋放內(nèi)存。運行時會為你完成所有這些工作,因此你可以專注于解決實際問題,而不是爭論機器級細節(jié)。
盡管如此,即使是經(jīng)驗不多的 Python 用戶,了解 Python 的垃圾收集和內(nèi)存管理是如何工作的也是有好處的。了解這些機制將幫助你避免更復(fù)雜的項目可能出現(xiàn)的性能問題。你還可以使用 Python 的內(nèi)置工具來監(jiān)控程序的內(nèi)存管理行為。
Python如何管理內(nèi)存
每個 Python 對象都有一個引用計數(shù),也稱為引用計數(shù)。 refcount 是持有對給定對象的引用的其他對象總數(shù)的計數(shù)。當你添加或刪除對對象的引用時,數(shù)字會上升或下降。當一個對象的引用計數(shù)變?yōu)榱銜r,該對象將被釋放并釋放其內(nèi)存。
什么是參考?允許通過名稱或通過另一個對象中的訪問器訪問對象的任何內(nèi)容。
這是一個簡單的例子:
x = "Hello there"
當我們向 Python 發(fā)出這個命令時,引擎蓋下會發(fā)生兩件事:
- 該字符串"Hello there"作為 Python 對象創(chuàng)建并存儲在內(nèi)存中。
- 該名稱x在本地命名空間中創(chuàng)建并指向該對象,這會將其引用計數(shù)增加 1 到 1。
如果我們說y = x,那么引用計數(shù)將再次提高到 2。
每當xandy超出范圍或從它們的命名空間中刪除時,對于每個名稱,字符串的引用計數(shù)都會減少 1。一旦x和y都超出范圍或被刪除,字符串的引用計數(shù)變?yōu)?0 并被刪除。
現(xiàn)在,假設(shè)我們創(chuàng)建了一個包含字符串的列表,如下所示:
x = ["Hello there", 2, False]
字符串保留在內(nèi)存中,直到列表本身被刪除或包含字符串的元素從列表中刪除。這些操作中的任何一個都將導(dǎo)致唯一持有對字符串的引用的事物消失。
現(xiàn)在考慮這個例子:
x = "Hello there" y = [x]
如果我們從 中刪除第一個元素y,或者完全刪除列表y,則字符串仍在內(nèi)存中。這是因為名稱x包含對它的引用。
Python 中的引用循環(huán)
在大多數(shù)情況下,引用計數(shù)工作正常。但有時你會遇到兩個對象各自持有對彼此的引用的情況。這稱為 參考周期。在這種情況下,對象的引用計數(shù)永遠不會達到零,也永遠不會從內(nèi)存中刪除。
這是一個人為的例子:
x = SomeClass()
y = SomeOtherClass()
x.item = y
y.item = x
由于x并y持有彼此的引用,因此它們永遠不會從系統(tǒng)中刪除——即使沒有其他任何東西引用它們中的任何一個。
Python 自己的運行時為對象生成引用循環(huán)實際上是相當普遍的。一個示例是帶有包含對異常本身的引用的回溯對象的異常。
在Python的早期版本中,這是一個問題。具有引用周期的對象可能會隨著時間的推移而累積,這對于長時間運行的應(yīng)用程序來說是一個大問題。但 Python 此后引入了循環(huán)檢測和垃圾收集系統(tǒng),用于管理引用循環(huán)。
Python 垃圾收集器 (gc)
Python 的垃圾收集器檢測具有引用周期的對象。它通過跟蹤作為“容器”的對象(例如列表、字典、自定義類實例)并確定其中的哪些對象無法在其他任何地方訪問來實現(xiàn)這一點。
一旦這些對象被挑選出來,垃圾收集器就會通過確保它們的引用計數(shù)可以安全地降為零來刪除它們。
絕大多數(shù) Python 對象沒有引用周期,因此垃圾收集器不需要 24/7 運行。相反,垃圾收集器使用一些啟發(fā)式方法來減少運行頻率,并且每次都盡可能高效地運行。
當 Python 解釋器啟動時,它會跟蹤已分配但未釋放的對象數(shù)量。絕大多數(shù) Python 對象的生命周期都很短,因此它們會迅速出現(xiàn)和消失。但隨著時間的推移,更多長壽的物體會出現(xiàn)。一旦超過一定數(shù)量的此類對象堆積起來,垃圾收集器就會運行。
每次垃圾收集器運行時,它都會收集所有在收集中幸存下來的對象,并將它們放在一個稱為一代的組中。這些“第一代”對象在參考周期中被掃描的頻率較低。任何在垃圾收集器中幸存下來的第一代對象最終都會遷移到第二代,在那里它們被掃描得更少。
同樣,垃圾收集器不會跟蹤所有內(nèi)容。例如,像用戶創(chuàng)建的類這樣的復(fù)雜對象總是被跟蹤。但是不會跟蹤僅包含簡單對象(如整數(shù)和字符串)的字典,因為該特定字典中的任何對象都不會包含對其他對象的引用。不能保存對其他元素(如整數(shù)和字符串)的引用的簡單對象永遠不會被跟蹤。
如何使用 gc 模塊
通常,垃圾收集器不需要調(diào)整即可運行良好。Python 的開發(fā)團隊選擇了反映最常見現(xiàn)實世界場景的默認值。但是如果你確實需要調(diào)整垃圾收集的工作方式,你可以使用Python 的 gc 模塊。該gc模塊為垃圾收集器的行為提供編程接口,并提供對正在跟蹤的對象的可見性。
gc當你確定不需要垃圾收集器時,你可以做的一件有用的事情是關(guān)閉它。例如,如果你有一個堆放大量對象的短運行腳本,則不需要垃圾收集器。腳本結(jié)束時,所有內(nèi)容都將被清除。為此,你可以使用命令禁用垃圾收集器gc.disable()。稍后,你可以使用 重新啟用它gc.enable()。
你還可以使用 手動運行收集周期gc.collect()。一個常見的應(yīng)用是管理程序的性能密集型部分,該部分會生成許多臨時對象。你可以在程序的該部分禁用垃圾收集,然后在最后手動運行收集并重新啟用收集。
另一個有用的垃圾收集優(yōu)化是gc.freeze(). 發(fā)出此命令時,垃圾收集器當前跟蹤的所有內(nèi)容都被“凍結(jié)”,或者被列為免于將來的收集掃描。這樣,未來的掃描可以跳過這些對象。如果你有一個程序在啟動之前導(dǎo)入庫并設(shè)置大量內(nèi)部狀態(tài),那么你可以gc.freeze()在所有工作完成后發(fā)出。這使垃圾收集器不必搜尋那些無論如何都不太可能被刪除的東西。(如果你想對凍結(jié)的對象再次執(zhí)行垃圾收集,請使用gc.unfreeze().)
使用 gc 調(diào)試垃圾收集
你還可以使用它gc來調(diào)試垃圾收集行為。如果你有過多的對象堆積在內(nèi)存中并且沒有被垃圾收集,你可以使用gc's 檢查工具來找出可能持有對這些對象的引用的對象。
如果你想知道哪些對象持有對給定對象的引用,可以使用gc.get_referrers(obj)列出它們。你還可以使用gc.get_referents(obj)來查找給定對象引用的任何對象。
如果你不確定給定對象是否是垃圾收集的候選對象,gc.is_tracked(obj)請告訴你垃圾收集器是否跟蹤該對象。如前所述,請記住垃圾收集器不會跟蹤“原子”對象(例如整數(shù))或僅包含原子對象的元素。
如果你想親自查看正在收集哪些對象,可以使用 設(shè)置垃圾收集器的調(diào)試標志gc.set_debug(gc.DEBUG_LEAK|gc.DEBUG_STATS)。這會將有關(guān)垃圾收集的信息寫入stderr。它將所有作為垃圾收集的對象保留在只讀列表中。
避免 Python 內(nèi)存管理中的陷阱
如前所述,如果你在某處仍有對它們的引用,則對象可能會堆積在內(nèi)存中而不會被收集。這并不是 Python 垃圾收集本身的失敗。垃圾收集器無法判斷你是否不小心保留了對某物的引用。
讓我們以一些防止對象永遠不會被收集的指針作為結(jié)尾。
注意對象范圍
如果你將對象 1 指定為對象 2 的屬性(例如類),則對象 2 將需要超出范圍,然后對象 1 才會:
obj1 = MyClass()
obj2.prop = obj1
更重要的是,如果這種情況發(fā)生在某種其他操作的副作用中,例如將對象 2 作為參數(shù)傳遞給對象 1 的構(gòu)造函數(shù),你可能不會意識到對象 1 持有一個引用:
obj1 = MyClass(obj2)
另一個例子:如果你將一個對象推入模塊級列表并忘記該列表,則該對象將一直保留,直到從列表中刪除,或者直到列表本身不再有任何引用。但是如果該列表是一個模塊級對象,它可能會一直存在,直到程序終止。
簡而言之,請注意你的對象可能被另一個看起來并不總是很明顯的對象持有的方式。
使用 weakref避免引用循環(huán)
Python 的 weakref 模塊允許你創(chuàng)建對其他對象的弱引用。弱引用不會增加對象的引用計數(shù),因此只有弱引用的對象是垃圾回收的候選對象。
一個常見的用途weakref是對象緩存。你不希望僅僅因為它具有緩存條目而保留引用的對象,因此你將 aweakref用于緩存條目。
手動中斷參考循環(huán)
最后,如果你知道給定對象包含對另一個對象的引用,你總是可以手動中斷對該對象的引用。例如,如果你有instance_of_class.ref = other_object,你可以設(shè)置instance_of_class.ref = None何時準備刪除 instance_of_class。
通過了解 Python 內(nèi)存管理的工作原理,我們對其垃圾收集系統(tǒng)如何幫助優(yōu)化 Python 程序中的內(nèi)存,以及如何使用標準庫和其他地方提供的模塊來控制內(nèi)存使用和垃圾收集。