Python 垃圾回收機(jī)制

? ? ? ?一些高級語言,比如Java、C#都會有垃圾回收機(jī)制,防止一些沒有的空間占用過多的內(nèi)存,最后導(dǎo)致程序宕掉,c,c++里用戶自己管理維護(hù)內(nèi)存的方式,內(nèi)存的申請、釋放需要用戶手動操作。Python底層也制定了垃圾回收機(jī)制,因此普通用戶在使用Python時,不用關(guān)心何時開啟、執(zhí)行垃圾回收機(jī)制。

Python的垃圾回收機(jī)制主要采用的是引用計(jì)數(shù)為主、標(biāo)記清除隔代回收為輔的垃圾回收策略。

小整數(shù)對象池

整數(shù)在程序的使用中會非常頻繁,因此Python在程序運(yùn)行之前,會在內(nèi)存中創(chuàng)建出小整數(shù)對象池供用戶使用,防止頻繁操作整數(shù)對象進(jìn)行內(nèi)存的申請與銷毀。

Python小整數(shù)對象池的范圍是[-5, 256],這些整數(shù)對象都是提前創(chuàng)建好的,在這個范圍內(nèi)的整數(shù)獨(dú)占內(nèi)存,不會被垃圾回收。


小整數(shù)對象池舉例1


小整數(shù)對象池舉例2

超過這個范圍,Python就會創(chuàng)建新的對象,開辟新的內(nèi)存空間:

小整數(shù)對象池舉例3

intern機(jī)制

a = "HelloPython"

b = "HelloPython"

c = "HelloPython"

類似這樣的,Python去不會創(chuàng)建3個字符串對象,而是會默認(rèn)開啟intern機(jī)制,將這三個對象指向同一個內(nèi)存空間,節(jié)省內(nèi)存開銷。

intern機(jī)制舉例1

但是如果字符串中存在特殊字符,情況會有例外,intern機(jī)制就會失效:

intern機(jī)制舉例2

引用計(jì)數(shù)機(jī)制

Python中,每一個東西都是對象,底層實(shí)現(xiàn)中都是一個PyObject:


其中,ob_refcnt屬性記錄著該對象的引用次數(shù),當(dāng)有東西引用該對象時,該屬性會加1,引用該對象被刪除時,該屬性會減1,如果引用次數(shù)為0,就會觸發(fā)垃圾回收機(jī)制

引用計(jì)數(shù)機(jī)制的優(yōu)點(diǎn):簡單 實(shí)時性,一旦沒有引用,內(nèi)存就直接釋放了。不用像其他機(jī)制等到特定時機(jī)。實(shí)時性還帶來一個好處:處理回收內(nèi)存的時間分?jǐn)偟搅似綍r。

引用計(jì)數(shù)機(jī)制的缺點(diǎn):引用計(jì)數(shù)耗費(fèi)資源;出現(xiàn)循環(huán)引用時,會出現(xiàn)問題


循環(huán)引用


圖示引用計(jì)數(shù)缺點(diǎn)

t1與t2形成了循環(huán)引用,刪除t1與t2之后,仍有引用計(jì)數(shù),會導(dǎo)致內(nèi)存空間不斷被占用,進(jìn)而導(dǎo)致宕機(jī)

為了解決這一問題,Python又引入了標(biāo)記清除與隔代回收機(jī)制

標(biāo)記清除機(jī)制

請注意在以上剛剛說到的例子中,我們以一個不是很常見的情況結(jié)尾:我們有一個“孤島”或是一組未使用的、互相指向的對象,但是誰都沒有外部引用。換句話說,我們的程序不再使用這些節(jié)點(diǎn)對象了,所以我們希望Python的垃圾回收機(jī)制能夠足夠智能去釋放這些對象并回收它們占用的內(nèi)存空間。但是這不可能,因?yàn)樗械囊糜?jì)數(shù)都是1而不是0。Python的引用計(jì)數(shù)算法不能夠處理互相指向自己的對象。

“標(biāo)記-清除”就是為了解決循環(huán)引用的問題??梢园渌麑ο笠玫娜萜鲗ο螅ū热纾簂ist,set,dict,class,instance)都可能產(chǎn)生循環(huán)引用。

我們必須承認(rèn)上面的事實(shí),如果兩個對象的引用計(jì)數(shù)都為1,但是僅僅存在他們之間的循環(huán)引用,那么這兩個對象都是需要被回收的,也就是說,它們的引用計(jì)數(shù)雖然表現(xiàn)為非0,但實(shí)際上有效的引用計(jì)數(shù)為0。我們必須先將循環(huán)引用摘掉,那么這兩個對象的有效計(jì)數(shù)就現(xiàn)身了。假設(shè)兩個對象為A、B,我們從A出發(fā),因?yàn)樗幸粋€對B的引用,則將B的引用計(jì)數(shù)減1;然后順著引用達(dá)到B,因?yàn)锽有一個對A的引用,同樣將A的引用減1,這樣,就完成了循環(huán)引用對象間環(huán)摘除。

但是這樣就有一個問題,假設(shè)對象A有一個對象引用C,而C沒有引用A,如果將C計(jì)數(shù)引用減1,而最后A并沒有被回收,顯然,我們錯誤的將C的引用計(jì)數(shù)減1,這將導(dǎo)致在未來的某個時刻出現(xiàn)一個對C的懸空引用。這就要求我們必須在A沒有被刪除的情況下復(fù)原C的引用計(jì)數(shù),如果采用這樣的方案,那么維護(hù)引用計(jì)數(shù)的復(fù)雜度將成倍增加。

原理:“標(biāo)記-清除”采用了更好的做法,我們并不改動真實(shí)的引用計(jì)數(shù),而是將集合中對象的引用計(jì)數(shù)復(fù)制一份副本,改動該對象引用的副本。對于副本做任何的改動,都不會影響到對象生命走起的維護(hù)。

這個計(jì)數(shù)副本的唯一作用是尋找root object集合(該集合中的對象是不能被回收的)。當(dāng)成功尋找到root object集合之后,首先將現(xiàn)在的內(nèi)存鏈表一分為二,一條鏈表中維護(hù)root object集合,成為root鏈表,而另外一條鏈表中維護(hù)剩下的對象,成為unreachable鏈表。之所以要剖成兩個鏈表,是基于這樣的一種考慮:現(xiàn)在的unreachable可能存在被root鏈表中的對象,直接或間接引用的對象,這些對象是不能被回收的,一旦在標(biāo)記的過程中,發(fā)現(xiàn)這樣的對象,就將其從unreachable鏈表中移到root鏈表中;當(dāng)完成標(biāo)記后,unreachable鏈表中剩下的所有對象就是名副其實(shí)的垃圾對象了,接下來的垃圾回收只需限制在unreachable鏈表中即可。

那么GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?

對象之間通過引用(指針)連在一起,構(gòu)成一個有向圖,對象構(gòu)成這個有向圖的節(jié)點(diǎn),而引用關(guān)系構(gòu)成這個有向圖的邊。從根對象(root object)出發(fā),沿著有向邊遍歷對象,可達(dá)的(reachable)對象標(biāo)記為活動對象,不可達(dá)的對象就是要被清除的非活動對象。根對象就是全局變量、調(diào)用棧、寄存器。

隔代回收機(jī)制

Python使用一種不同的鏈表來持續(xù)追蹤活躍的對象。而不將其稱之為“活躍列表”,Python的內(nèi)部C代碼將其稱為零代(Generation Zero)。每次當(dāng)你創(chuàng)建一個對象或其他什么值的時候,Python會將其加入零代鏈表:


從上邊可以看到當(dāng)我們創(chuàng)建ABC節(jié)點(diǎn)的時候,Python將其加入零代鏈表。請注意到這并不是一個真正的列表,并不能直接在你的代碼中訪問,事實(shí)上這個鏈表是一個完全內(nèi)部的Python運(yùn)行時。 相似的,當(dāng)我們創(chuàng)建DEF節(jié)點(diǎn)的時候,Python將其加入同樣的鏈表:


現(xiàn)在零代包含了兩個節(jié)點(diǎn)對象。(他還將包含Python創(chuàng)建的每個其他值,與一些Python自己使用的內(nèi)部值。)

檢測循環(huán)引用

隨后,Python會循環(huán)遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據(jù)規(guī)則減掉其引用計(jì)數(shù)。在這個過程中,Python會一個接一個的統(tǒng)計(jì)內(nèi)部引用的數(shù)量以防過早地釋放對象。

為了便于理解,來看一個例子:


從上面可以看到 ABC 和 DEF 節(jié)點(diǎn)包含的引用數(shù)為1.有三個其他的對象同時存在于零代鏈表中,藍(lán)色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。(接下來我們會看到,Python中同時存在另外兩個分別被稱為一代和二代的鏈表)。這些對象有著更高的引用計(jì)數(shù)因?yàn)樗鼈冋诒黄渌羔標(biāo)赶蛑?/p>

接下來你會看到Python的GC是如何處理零代鏈表的。



導(dǎo)致引用計(jì)數(shù)+1的情況

1、對象被創(chuàng)建,例如a=23?

2、對象被引用,例如b=a?

3、對象被作為參數(shù),傳入到一個函數(shù)中,例如func(a)?

4、對象作為一個元素,存儲在容器中,例如list1=[a,a]

導(dǎo)致引用計(jì)數(shù)-1的情況

1、對象的別名被顯式銷毀,例如del a?

2、對象的別名被賦予新的對象,例如a=24?

3、一個對象離開它的作用域,例如f函數(shù)執(zhí)行完畢時,func函數(shù)中的局部變量(全局變量不會)?

4、對象所在的容器被銷毀,或從容器中刪除對象

GC常用函數(shù):

1、gc.set_debug(flags)設(shè)置gc的debug日志,一般設(shè)置為gc.DEBUG_LEAK

2、gc.collect([generation])顯式進(jìn)行垃圾回收,可以輸入?yún)?shù),0代表只檢查第一代的對象,1代表檢查一,二代的對象,2代表檢查一,二,三代的對象,如果不傳參數(shù),執(zhí)行一個full collection,也就是等于傳2。 返回不可達(dá)(unreachable objects)對象的數(shù)目

3、gc.get_threshold()獲取的gc模塊中自動執(zhí)行垃圾回收的頻率。

4、gc.set_threshold(threshold0[, threshold1[, threshold2])設(shè)置自動執(zhí)行垃圾回收的頻率。

5、gc.get_count()獲取當(dāng)前自動執(zhí)行垃圾回收的計(jì)數(shù)器,返回一個長度為3的列表

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容