一、概述
python采用的是引用計數(shù)機制為主,標(biāo)記-清除和分代收集(隔代回收)兩種機制為輔的策略。
現(xiàn)在的高級語言如java,c#等,都采用了垃圾收集機制,而不再是c,c++里用戶自己管理維護內(nèi)存的方式。自己管理內(nèi)存極其自由,可以任意申請內(nèi)存,但如同一把雙刃劍,為大量內(nèi)存泄露,懸空指針等bug埋下隱患。
對于一個字符串、列表、類甚至數(shù)值都是對象,且定位簡單易用的語言,自然不會讓用戶去處理如何分配回收內(nèi)存的問題。
python里也同java一樣采用了垃圾收集機制,不過不一樣的是:
python采用的是引用計數(shù)機制為主,標(biāo)記-清除和分代收集(隔代回收)兩種機制為輔的策略。
二、引用計數(shù)機制
引用計數(shù)法機制的原理是:每個對象維護一個ob_ref字段,用來記錄該對象當(dāng)前被引用的次數(shù),每當(dāng)新的引用指向該對象時,它的引用計數(shù)ob_ref加1,每當(dāng)該對象的引用失效時計數(shù)ob_ref減1,一旦對象的引用計數(shù)為0,該對象立即被回收,對象占用的內(nèi)存空間將被釋放。它的缺點是需要額外的空間維護引用計數(shù),這個問題是其次的,不過最主要的問題是它不能解決對象的“循環(huán)引用”,因此,也有很多語言比如Java并沒有采用該算法做來垃圾的收集機制。
python里每一個東西都是對象,它們的核心就是一個結(jié)構(gòu)體:PyObject
PyObject是每個對象必有的內(nèi)容,其中ob_refcnt就是做為引用計數(shù)。當(dāng)一個對象有新的引用時,它的ob_refcnt就會增加,當(dāng)引用它的對象被刪除,它的ob_refcnt就會減少
引用計數(shù)為0時,該對象生命就結(jié)束了。
引用計數(shù)機制的優(yōu)點:
1、簡單
2、實時性:一旦沒有引用,內(nèi)存就直接釋放了,不用像其他機制得等到特定時機。實時性還帶來一個好處:處理回收內(nèi)存的時間分?jǐn)偟搅似綍r。
引用計數(shù)機制的缺點:
1、維護引用計數(shù)消耗資源
2、循環(huán)引用
案例:
import sys
class A():
def __init__(self):
'''初始化對象'''
print('object born id:%s' %str(hex(id(self))))
def f1():
'''循環(huán)引用變量與刪除變量'''
while True:
c1=A()
del c1
def func(c):
print('obejct refcount is: ',sys.getrefcount(c)) #getrefcount()方法用于返回對象的引用計數(shù)
if __name__ == '__main__':
#生成對象
a=A()
func(a)
#增加引用
b=a
func(a)
#銷毀引用對象b
del b
func(a)
#結(jié)果
object born id:0x19f5ecb9320
obejct refcount is: 4
obejct refcount is: 5
obejct refcount is: 4
導(dǎo)致引用計數(shù)+1的情況
- 對象被創(chuàng)建,例如a=23
- 對象被引用,例如b=a
- 對象被作為參數(shù),傳入到一個函數(shù)中,例如
func(a) - 對象作為一個元素,存儲在容器中,例如
list1=[a,a]
導(dǎo)致引用計數(shù)-1的情況
- 對象的別名被顯式銷毀,例如
del a - 對象的別名被賦予新的對象,例如
a=24 - 一個對象離開它的作用域,例如:func函數(shù)執(zhí)行完畢時,func函數(shù)中的局部變量(全局變量不會)
- 對象所在的容器被銷毀,或從容器中刪除對象
循環(huán)引用導(dǎo)致內(nèi)存泄露
def f2():
'''循環(huán)引用'''
while True:
c1=A()
c2=A()
c1.t=c2
c2.t=c1
del c1
del c2
- 創(chuàng)建了
c1,c2后,這兩個對象的引用計數(shù)都是1,執(zhí)行c1.t=c2和c2.t=c1后,引用計數(shù)變成2. - 在
del c1后,內(nèi)存c1的對象的引用計數(shù)變?yōu)?code>1,由于不是為0,所以c1的對象不會被銷毀,同理,在del c2后也是一樣的。 - 雖然它們兩個的對象都是可以被銷毀的,但是由于循環(huán)引用,導(dǎo)致垃圾回收器都不會回收它們,所以就會導(dǎo)致內(nèi)存泄露。
分代回收
- 分代回收是一種以空間換時間的操作方式,Python將內(nèi)存根據(jù)對象的存活時間劃分為不同的集合,每個集合稱為一個代,Python將內(nèi)存分為了3“代”,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應(yīng)的是3個鏈表,它們的垃圾收集頻率隨著對象存活時間的增大而減小。
- 新創(chuàng)建的對象都會分配在年輕代,年輕代鏈表的總數(shù)達到上限時,Python垃圾收集機制就會被觸發(fā),把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活于整個系統(tǒng)的生命周期內(nèi)。
- 同時,分代回收是建立在標(biāo)記清除技術(shù)基礎(chǔ)之上。分代回收同樣作為Python的輔助垃圾收集技術(shù)處理那些容器對象
垃圾回收
有三種情況會觸發(fā)垃圾回收:
- 調(diào)用
gc.collect(),需要先導(dǎo)入gc模塊。 - 當(dāng)
gc模塊的計數(shù)器達到閾值的時候。 - 程序退出的時候。
gc模塊
gc模塊提供一個接口給開發(fā)者設(shè)置垃圾回收的選項。上面說到,采用引用計數(shù)的方法管理內(nèi)存的一個缺陷是循環(huán)引用,而gc模塊的一個主要功能就是解決循環(huán)引用的問題。
常用函數(shù):
-
gc.set_debug(flags)設(shè)置gc的debug日志,一般設(shè)置為gc.DEBUG_LEAK -
gc.collect([generation])
顯式進行垃圾回收,可以輸入?yún)?shù),0代表只檢查第一代的對象,1代表檢查一,二代的對象,2代表檢查一,二,三代的對象,如果不傳參數(shù),執(zhí)行一個full collection,也就是等于傳2。返回不可達(unreachable objects)對象的數(shù)目。 -
gc.set_threshold(threshold0[, threshold1[, threshold2])
設(shè)置自動執(zhí)行垃圾回收的頻率。 -
gc.get_count()獲取當(dāng)前自動執(zhí)行垃圾回收的計數(shù)器,返回一個長度為3的列表
gc實踐案例
def f3():
'''循環(huán)引用'''
while True:
c1=A()
c2=A()
c1.t=c2
c2.t=c1
del c1
del c2
#增加垃圾回收機制
print(gc.garbage)
print(gc.collect())
print(gc.garbage)
time.sleep(10)
#結(jié)果
object born id:0x21d1a5dc470
object born id:0x21d1a5dc9e8
[]
4
gc: collectable <A 0x0000021D1A5DC470>
[<__main__.A object at 0x0000021D1A5DC470>, <__main__.A object at 0x0000021D1A5DC9E8>, {'t': <__main__.A object at 0x0000021D1A5DC9E8>}, {'t': <__main__.A object at 0x0000021D1A5DC470>}]
gc: collectable <A 0x0000021D1A5DC9E8>
gc: collectable <dict 0x0000021D1A156C88>
gc: collectable <dict 0x0000021D1A5CABC8>
gc模塊的自動垃圾回收機制
必須要import gc模塊,并且is_enable()=True才會啟動自動垃圾回收。
這個機制的主要作用就是發(fā)現(xiàn)并處理不可達的垃圾對象。
垃圾回收=垃圾檢查+垃圾回收
在Python中,采用分代收集的方法。把對象分為三代,一開始,對象在創(chuàng)建的時候,放在一代中,如果在一次一代的垃圾檢查中,該對象存活下來,就會被放到二代中,同理在一次二代的垃圾檢查中,該對象存活下來,就會被放到三代中。
gc模塊里面會有一個長度為3的列表的計數(shù)器,可以通過gc.get_count()獲取。
def f4():
'''垃圾自動回收'''
print(gc.get_count())
a=A()
print(gc.get_count())
del a
print(gc.get_count())
#結(jié)果
(621, 10, 0)
object born id:0x2ca32a8c588
(624, 10, 0)
(623, 10, 0)
-
621指距離上一次一代垃圾檢查,Python分配內(nèi)存的數(shù)目減去釋放內(nèi)存的數(shù)目,注意:是內(nèi)存分配,而不是引用計數(shù)的增加。 -
10指距離上一次二代垃圾檢查,一代垃圾檢查的次數(shù)。 -
0是指距離上一次三代垃圾檢查,二代垃圾檢查的次數(shù)。
自動回收閾值
gc模快有一個自動垃圾回收的閥值,即通過gc.get_threshold函數(shù)獲取到的長度為3的元組,例如(700,10,10)
每一次計數(shù)器的增加,gc模塊就會檢查增加后的計數(shù)是否達到閥值的數(shù)目,如果是,就會執(zhí)行對應(yīng)的代數(shù)的垃圾檢查,然后重置計數(shù)器
注意:
如果循環(huán)引用中,兩個對象都定義了__del__方法,gc模塊不會銷毀這些不可達對象,因為gc模塊不知道應(yīng)該先調(diào)用哪個對象的__del__方法,所以為了安全起見,gc模塊會把對象放到gc.garbage中,但是不會銷毀對象。
標(biāo)記清除
標(biāo)記清除(Mark—Sweep)』算法是一種基于追蹤回收(tracing GC)技術(shù)實現(xiàn)的垃圾回收算法。它分為兩個階段:第一階段是標(biāo)記階段,GC會把所有的『活動對象』打上標(biāo)記,第二階段是把那些沒有標(biāo)記的對象『非活動對象』進行回收。那么GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?

對象之間通過引用(指針)連在一起,構(gòu)成一個有向圖,對象構(gòu)成這個有向圖的節(jié)點,而引用關(guān)系構(gòu)成這個有向圖的邊。從根對象(root object)出發(fā),沿著有向邊遍歷對象,可達的(reachable)對象標(biāo)記為活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調(diào)用棧、寄存器。 mark-sweepg 在上圖中,我們把小黑圈視為全局變量,也就是把它作為root object,從小黑圈出發(fā),對象1可直達,那么它將被標(biāo)記,對象2、3可間接到達也會被標(biāo)記,而4和5不可達,那么1、2、3就是活動對象,4和5是非活動對象會被GC回收。
標(biāo)記清除算法作為Python的輔助垃圾收集技術(shù)主要處理的是一些容器對象,比如list、dict、tuple,instance等,因為對于字符串、數(shù)值對象是不可能造成循環(huán)引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標(biāo)記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內(nèi)存,哪怕只剩下小部分活動對象也要掃描所有對象。