Python 垃圾回收機制

Garbage collection(GC)

現在的高級語言如java,c#等,都採用了垃圾收集機制,而不再是c,c++裏用戶自己管理維護內存的方式。自己管理內存極其自由,可以任意申請內存,但如同一把雙刃劍,爲大量內存泄露,懸空指針等bug埋下隱患。
對於一個字符串、列表、類甚至數值都是對象,且定位簡單易用的語言,自然不會讓用戶去處理如何分配回收內存的問題。
python裏也同java一樣採用了垃圾收集機制,不過不一樣的是:
python採用的是引用計數機制爲主,標記-清除和分代收集兩種機制爲輔的策略
1、引用計數機制:

python裏每一個東西都是對象,它們的核心就是一個結構體:PyObject

 typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject是每個對象必有的內容,其中ob_refcnt就是做爲引用計數。當一個對象有新的引用時,它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加計數
#define Py_DECREF(op) \ //減少計數
    if (--(op)->ob_refcnt != 0) \
        ; \
    else \
        __Py_Dealloc((PyObject *)(op))

當引用計數爲0時,該對象生命就結束了。

引用計數機制的優點:

簡單
實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。

引用計數機制的缺點:

維護引用計數消耗資源

循環引用

list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

list1與list2相互引用,如果不存在其他對象對它們的引用,list1與list2的引用計數也仍然爲1,所佔用的內存永遠無法被回收,這將是致命的。
對於如今的強大硬件,缺點1尚可接受,但是循環引用導致內存泄露,註定python還將引入新的回收機制。(標記清除和分代收集)
2、標記-清理

由上面內容我們可以知道,引用計數機制有兩個缺點,缺點1還可以勉強讓人接受,缺點2如果不解決,肯定會引起內存泄露,爲了解決這個問題,引入了標記刪除。

我們先來看個實例,從實例中領會標記刪除:

 a=[1,2]#假設此時a的引用爲1

b=[3,4]#假設此時b的引用爲1 #循環引用

a.append(b)#b的引用+1=2

b.append(a)//a的引用+1=2



假如現在需要刪除a,應該如何回收呢?(注意刪除a可以使用del a,這樣a這個引用就不存在了,但是它指向的對象,在標記刪除後還存在,因爲還被b使用者)

c=[5,6]#假設此時c的引用爲1

d=[7,8]#假設此時d的引用爲1 #循環引用

c.append(d)#c的引用+1=2

d.append(c)#d的引用+1=2 假如現在需要同時刪除c、d,應該如何回收呢?

首先我們應該已經知道,不管上面兩種情況的哪一個都無法只通過計數來完成回收,因爲隨便刪除一個變量,它的引用只會-1,變成1,還是大於0,不會回收,爲了解決這個問題,開始看標記刪除來大展神威吧。

python標記刪除時通過l兩個容器來完成的:死亡容器、存活容器。
 
首先,我們先來分析情況2,刪除c、d
刪除後,c的引用爲1,d的引用爲1,根據引用計數,還無法刪除
 
標記刪除第一步:對執行刪除操作後的每個引用-1,此時c的引用爲0,d的引用爲0,把他們都放到死亡容器內。把那些引用仍然大於0的放到存活容器內。
 
標記刪除第二步:遍歷存活容器,查看是否有的存活容器引用了死亡容器內的對象,如果有就把該對象(注意是對象,比如0x7f94bb602f80,不是對象的引用)從死亡容器內取出,放到存活容器內。
由於c、d都沒有對象引用他們了,所以經過這一步驟,他們還是在死亡組。
 
標記刪除第三部:將死亡組所有對象刪除。
這樣就完成了對從c、d的刪除。

同樣道理,我們來分析:只刪除a的過程:

標記刪除第一步:對執行刪除(-1)後的每個引用-1,那麼a的引用就是0,b的引用爲1,將a放到死亡容器,將b放到存活容器。
標記刪除第二步:循環存活容器,發現b引用a,復活a:將a放到存活容器內。
標記刪除第三步:刪除死亡容器內的所有對象。

綜上所說,發現對於循環引用,必須將循環引用的雙發對象都刪除,纔可以被回收。

標記-清理就是這樣。
3、分代收集

經過上面的【標記-清理】方法,已經可以保證對垃圾的回收了,但還有一個問題,【標記-清理】什麼時候執行比較好呢,是對所有對象都同時執行嗎?

同時執行很顯然不合理,我們知道,存活越久的對象,說明他的引用更持久(好像是個屁話,引用不持久就被刪除了),爲了更合理的進行【標記-刪除】,就需要對對象進行分代處理,思路很簡單:

1、新創建的對象做爲0代
2、每執行一個【標記-刪除】,存活的對象代數就+1
3、代數越高的對象(存活越持久的對象),進行【標記-刪除】的時間間隔就越長。這個間隔,江湖人稱閥值。

是不是很簡單呢。
4、三種情況觸發垃圾回收

1、調用gc.collect()
2、GC達到閥值時
3、程序退出時

5、小整數對象池與intern機制:這個機理是有的,但是下文的數據有待認證

由於整數使用廣泛,爲了避免爲整數頻繁銷燬、申請內存空間,引入了小整數對象池。[-5,257)是提前定義好的,不會銷燬,單個字母也是。

那對於其他整數,或者其他字符串的不可變類型,如果存在重複的多個,例如:

a1="mark"
a2="mark"
a3="mark"
a4="mark"
....
a1000="mark"

如果每次聲明都開闢出一段空間,很顯然不合理,這個時候python就會使用intern機制,靠引用計數來維護。

總計:

1、小整數[-5,257):共用對象,常駐內存
2、單個字符:共用對象,常駐內存
3、單個單詞等不可變類型,默認開啓intern機制,共用對象,引用計數爲0時銷燬。

————————————————
版權聲明:本文爲CSDN博主「LuckyQueen0928」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/luckyqueen0928/article/details/96044552

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章