python 垃圾回收机制
DAY 18. python垃圾回收機(jī)制
python GC主要有三種方式
- 引用計(jì)數(shù)
- 標(biāo)記清除
- 分代回收
其中,以引用計(jì)數(shù)為主。
18.1 引用計(jì)數(shù)(Reference Counting)
《尋夢(mèng)環(huán)游記》中說,人一生會(huì)經(jīng)歷兩次死亡,一次是肉體死的時(shí)候,另一次是最后一個(gè)記得你的人也忘了你時(shí),當(dāng)一個(gè)人沒有人記得的時(shí)候,才算真的死亡。垃圾回收也是這樣,當(dāng)最后一個(gè)對(duì)象的引用死亡時(shí),這個(gè)對(duì)象就會(huì)變成垃圾對(duì)象。
引用計(jì)數(shù)的原理是在每次創(chuàng)建對(duì)象時(shí)都添加一個(gè)計(jì)數(shù)器,每當(dāng)有引用指向這個(gè)對(duì)象時(shí),該計(jì)數(shù)器就會(huì)加1,當(dāng)引用結(jié)束計(jì)數(shù)器就會(huì)減1,當(dāng)計(jì)數(shù)器為0時(shí),該對(duì)象就會(huì)被回收。
python 中所有對(duì)象所共有的數(shù)據(jù)成員由一個(gè)叫做pyobject的結(jié)構(gòu)體來保存
typedef struct _object {/* 宏,僅僅在Debag模式下才不為空 */_PyObject_HEAD_EXTRA/* 定義了一個(gè) Py_ssize_t 類型的 ob_refcnt 用來計(jì)數(shù) */Py_ssize_t ob_refcnt;/* 類型 */struct _typeobject *ob_type; } PyObject;里面的ob_refcnt就是垃圾回收用到的計(jì)數(shù)器,而Py_ssize_t是整數(shù)
pyobject中保存的是python對(duì)象中共有的數(shù)據(jù)成員,所以python創(chuàng)建的每一個(gè)對(duì)象都會(huì)有該屬性。
在python中可以使用from sys import getrefcount來查看引用計(jì)數(shù)的值,但一般這個(gè)值會(huì)比期望的ob_refcnt高,應(yīng)為它會(huì)包含臨時(shí)應(yīng)用以作為getrefcount的參數(shù)
以下情況ob_refcnt加一
- 創(chuàng)建對(duì)象
- 引用對(duì)象
- 作為參數(shù)傳遞到函數(shù)中
- 作為成員存儲(chǔ)在容器中
以下情況,計(jì)數(shù)減一:
- 當(dāng)該對(duì)象的別名被顯式銷毀時(shí)
- 該對(duì)象的別名被賦予新值時(shí)
- 離開作用域時(shí)
- 從容器中刪除時(shí)
當(dāng)計(jì)數(shù)被減為0時(shí),該對(duì)象就會(huì)被回收
class MyList(list):def __del__(self):print('該對(duì)象被回收')s = MyList() s = [] # s是MyList實(shí)例對(duì)象唯一的引用,s指向別的對(duì)象,MyList的這個(gè)實(shí)例對(duì)象就會(huì)被立刻回收 print('end') # 該對(duì)象被回收 # end優(yōu)點(diǎn):
- 實(shí)現(xiàn)簡(jiǎn)單
- 內(nèi)存回收及時(shí),只要沒有引用立刻回收
- 高效對(duì)象有確定生命周期
缺點(diǎn):
- 維護(hù)計(jì)數(shù)器占用資源
- 無法解決循環(huán)引用問題
a和b相互引用,造成a,b的計(jì)數(shù)始終大于0,這樣就無法使用引用計(jì)數(shù)的方法處理垃圾,針對(duì)這種情況,python使用另外一種GC機(jī)制——標(biāo)記清除來回收垃圾。
18.2 標(biāo)記清除(Mark-Sweep)
標(biāo)記清除就是為解決循環(huán)引用產(chǎn)生的,應(yīng)為它造成的內(nèi)存開銷較大,所以在不會(huì)產(chǎn)生循環(huán)引用的對(duì)象上是不會(huì)使用的。
- 哪寫對(duì)象會(huì)產(chǎn)生循環(huán)引用?
只有能“引用”別的對(duì)象,才會(huì)產(chǎn)生循環(huán)引用,那些int,string等是不會(huì)產(chǎn)生的,只有“容器”,類似list,dict,class之類才可能產(chǎn)生,也只有這類對(duì)象才可能使用標(biāo)記清除機(jī)制。
過程:
- 去環(huán)
- 計(jì)數(shù)為0的加入生存組,不為零的加入死亡組
- 生存組中的元素作為root,root的可達(dá)節(jié)點(diǎn)從死亡組中提出
- 回收死亡組中的對(duì)象
原理:
from sys import getrefcountclass MyList(list):def __del__(self):print('該對(duì)象被回收')a = MyList() b = MyList() a.append(b) b.append(a) print(f'a的引用計(jì)數(shù){getrefcount(a)}') print(f'b的引用計(jì)數(shù){getrefcount(b)}') del a print(f'del a的引用計(jì)數(shù){getrefcount(b[0])}')c = MyList() d = MyList() c.append(d) d.append(c) print(f'c的引用計(jì)數(shù){getrefcount(c)}') print(f'd的引用計(jì)數(shù){getrefcount(d)}') del c del dprint('end')這是一開始a,b 的情況
他們的計(jì)數(shù)都是2,cd也一樣,使用del語(yǔ)句會(huì)斷開變量ab與MyList()內(nèi)存之間的聯(lián)系
這個(gè)時(shí)候就該標(biāo)記清除上場(chǎng)了,由于a還存在,而a中引用了b,cd相互引用但都通過del顯式清除了,所以經(jīng)過標(biāo)記清除,ab會(huì)被保留,cd會(huì)被清除。
標(biāo)記清除的第一步是“標(biāo)記”,通過兩個(gè)容器來實(shí)現(xiàn)————生存容器和死亡容器,python首先會(huì)檢測(cè)循環(huán)引用,這時(shí)會(huì)將所有對(duì)象的計(jì)數(shù)復(fù)制一個(gè)副本以避免破壞真實(shí)的引用計(jì)數(shù)值,然后檢查鏈表中每個(gè)相互引用的對(duì)象,把這些對(duì)象的計(jì)數(shù)都減一,這一步叫做去環(huán)。
上面ab,cd都相互引用,經(jīng)過del之后,a的計(jì)數(shù)依舊是2,bcd的計(jì)數(shù)是1,去環(huán)以后a的計(jì)數(shù)是1,bcd計(jì)數(shù)為0。
經(jīng)過去環(huán)以后,將所有計(jì)數(shù)為0的值(bcd)加入死亡容器,不為0的(a)加入生存容器,這時(shí)還不能直接清除死亡容器中的對(duì)象,需要二審,先遍歷生存容器中的對(duì)象,把每一個(gè)生存容器中的值作為root object,根據(jù)該對(duì)象的引用關(guān)系建立有向圖,可達(dá)節(jié)點(diǎn)就標(biāo)記為活動(dòng)對(duì)象,不可達(dá)節(jié)點(diǎn)就為非活動(dòng)對(duì)象(就是查看生存容器中是否引用了死亡容器中的對(duì)象,如果有,就把這個(gè)對(duì)象從死亡容器解救到生存容器)。
這里a引用了死亡容器中的b,所以b會(huì)被解救。
最后,死亡容器中的對(duì)象會(huì)被清除。
- 什么時(shí)候進(jìn)行標(biāo)記清除
標(biāo)記清除并不像引用計(jì)數(shù)那樣是實(shí)時(shí)的,而是等待占用內(nèi)存到達(dá)GC閾值的時(shí)候才會(huì)觸發(fā)
18.3 分代回收
上面說了標(biāo)記回收通過生存和死亡兩個(gè)容器來實(shí)現(xiàn),但這只是為了方便理解說的,在真實(shí)情況下,標(biāo)記清除是依賴分代回收計(jì)數(shù)完成的。
首先,我們?cè)趐ython中創(chuàng)建的每一個(gè)對(duì)象都會(huì)被收納進(jìn)一個(gè)鏈表中,python稱其為零代(Generation Zero)經(jīng)過檢測(cè)循環(huán)引用,會(huì)按照規(guī)則減去有循環(huán)引用的節(jié)點(diǎn)的計(jì)數(shù)值,這時(shí)候部分節(jié)點(diǎn)的計(jì)數(shù)值大于0,也有部分節(jié)點(diǎn)計(jì)數(shù)值等于0,大于0的節(jié)點(diǎn)會(huì)被放入二代,等于0的節(jié)點(diǎn)經(jīng)過“白障算法(write barrier)”就是上面說的二審,通過的就會(huì)放在零代,不通過的就會(huì)被清除釋放。一段時(shí)間后,使用同樣的算法遍歷一代鏈表,計(jì)數(shù)大于0的放入二代鏈表,等于0的進(jìn)行白障算法檢測(cè),通過留在一代,否則釋放,python中只有這三代鏈表,根據(jù) “弱代假說”(新生的對(duì)象存活時(shí)間比較短,年老的對(duì)象存活時(shí)間一般較長(zhǎng))python GC 會(huì)將主要精力放在零代上,而觸發(fā)回收則是根據(jù)GC閾值決定的,GC閾值是被分配對(duì)象的計(jì)數(shù)值與被釋放對(duì)象的計(jì)數(shù)值之間的差異,一旦這個(gè)差異超過閾值,就會(huì)觸發(fā)零代算法,回收垃圾把剩余對(duì)象放在一代,一代也類似,但隨著代數(shù)增加,閾值會(huì)提高(弱代假說),也就是零代的垃圾回收最頻繁,一代次之,二代最少。
18.4 總結(jié)
- 為新創(chuàng)建的對(duì)象分配內(nèi)存
- 識(shí)別垃圾對(duì)象
- 回收垃圾對(duì)象的內(nèi)存
- 沒有對(duì)象引用
- 只相互引用,孤島
python GC機(jī)制由三部分組成:引用計(jì)數(shù),標(biāo)記清除,分代回收,其中引用計(jì)數(shù)為主。
- 引用計(jì)數(shù):python所有對(duì)象的共同屬性由pyobject結(jié)構(gòu)體保存,該結(jié)構(gòu)體中有一個(gè)int類型的成員ob_refcnt用來實(shí)現(xiàn)引用計(jì)數(shù)。計(jì)數(shù)為0時(shí)對(duì)象為垃圾對(duì)象,回收內(nèi)存。
- 計(jì)數(shù)加一的情況:創(chuàng)建對(duì)象,對(duì)象作為函數(shù)參數(shù)傳遞,對(duì)象作為成員保存到容器中,對(duì)象增加了一個(gè)引用
- 計(jì)數(shù)減一的情況:通過del顯式刪除對(duì)象,引用指向None或別的對(duì)象,從容器中彈出,跳出作用域如函數(shù)生命結(jié)束
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,實(shí)時(shí)回收內(nèi)存
- 缺點(diǎn):無法解決循環(huán)引用問題,開銷大
- 標(biāo)記清除和分代回收:是為了解決引用計(jì)數(shù)無法回收相互引用的問題
- 作用對(duì)象:只作用于可能產(chǎn)生相互引用的“容器對(duì)象”如list,dict,class
- 處理過程:創(chuàng)建對(duì)象->加入零代鏈表->到達(dá)閾值->檢測(cè)循環(huán)引用->循環(huán)引用的節(jié)點(diǎn)計(jì)數(shù)減少->計(jì)數(shù)大于0的加入一代鏈表,小于零的->白障->在一代鏈表中有他的引用->不清理,保留,沒有引用,清理釋放內(nèi)存。
- 弱代假說:新生的對(duì)象一般存活時(shí)間較短,年老對(duì)象存活時(shí)間較長(zhǎng)
總結(jié)
以上是生活随笔為你收集整理的python 垃圾回收机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spatial Transformer
- 下一篇: 使用Intel NCS算力棒 安装部署记