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