Python 中的垃圾回收機制

一、概述

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=c2c2.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ā)垃圾回收:

  1. 調(diào)用gc.collect(),需要先導(dǎo)入gc模塊。
  2. 當(dāng)gc模塊的計數(shù)器達到閾值的時候。
  3. 程序退出的時候。

gc模塊

gc模塊提供一個接口給開發(fā)者設(shè)置垃圾回收的選項。上面說到,采用引用計數(shù)的方法管理內(nèi)存的一個缺陷是循環(huán)引用,而gc模塊的一個主要功能就是解決循環(huán)引用的問題。

常用函數(shù)

  1. gc.set_debug(flags) 設(shè)置gc的debug日志,一般設(shè)置為gc.DEBUG_LEAK
  2. gc.collect([generation])
    顯式進行垃圾回收,可以輸入?yún)?shù),0代表只檢查第一代的對象,1代表檢查一,二代的對象,2代表檢查一,二,三代的對象,如果不傳參數(shù),執(zhí)行一個full collection,也就是等于傳2。返回不可達(unreachable objects)對象的數(shù)目。
  3. gc.set_threshold(threshold0[, threshold1[, threshold2])
    設(shè)置自動執(zhí)行垃圾回收的頻率。
  4. 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)存,哪怕只剩下小部分活動對象也要掃描所有對象。

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

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