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