1. 程式人生 > >python垃圾回收雜談

python垃圾回收雜談

當建立物件時Python立即向作業系統請求記憶體。每當物件的引用數減為0,Python垃圾回收器立刻挺身而出,立即將其釋放,把記憶體還給作業系統。
在Python中,每個物件都儲存了一個稱為引用計數的整數值,來追蹤到底有多少引用指向了這個物件。無論何時,如果我們程式中的一個變數或其他物件引用了目標物件,Python將會增加這個計數值,而當程式停止使用這個物件,則Python會減少這個計數值。一旦計數值被減到零,Python將會釋放這個物件以及回收相關記憶體空間。而對於建立的物件,無論存在迴圈引用與否,只有還在使用,沒有被釋放,就會慢慢的隨著分代的策略,慢慢進入2代(老年代)

通過頻繁的處理零代連結串列中的新物件,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新物件。同時只在很少的時候,當滿足閾值的條件,收集器才會去處理那些老變數。這種演算法的根源來自於弱代假說(weak generational hypothesis):這個假說由兩個觀點構成:首先是年輕的物件通常死的也快,而年老物件則很有可能存活更長的時間。


引用計檢視方法:sys.getrefcount?,在ipython裡使用問號檢視幫助資訊

In [17]: sys.getrecursionlimit?
Docstring:
getrecursionlimit()

Return the current value of the recursion limit, the maximum depth
of the Python interpreter stack.  This limit prevents infinite
recursion from causing an overflow of the C stack and crashing Python.
Type:      builtin_function_or_method

In [18]:


hex(id(a))檢視是否真的被回收

感覺 標記-清除 與 分代可以看成一個整體
標記-清除過程先從零代開始,清理最頻繁的也是零代,然後是第一代、第二代


Python中的迴圈引用總是發生在container物件之間, 所謂containser物件即是內部可持有對其他物件的引用: list/dict/class/instance等等,不會是int str
"標記-清除"是為了解決迴圈引用的問題.可以包含其他物件引用的容器物件(比如:list,set,dict,class,instance)都可能產生迴圈引用


新建立的物件都在0代,0代不被引用計數和標記清除演算法回收的物件會被放入1代,1代不被引用計數和標記清除演算法回收的物件會被放入2代


gc.get_threshold
gc.set_threshold正確理解,預設值[700,10,10]
0代的700表示新建立的物件超過700個則對0代進行垃圾回收,能回收的回收掉,不能回收的放到1代中。1代的預設閾值10表示記錄0代進行回收的次數,即0代回收次數超過10次,則對1代進行垃圾回收,能回收的回收,不能回收的放入2代;2代的10表示1代回收的次數閾值,當1代回收次數超過10次時,對2代進行回收,能回收的回收,不能回收的繼續放到2代裡,等待下一次回收。每一代回收完,就會變化值0

gc.collect()對所有代進行回收,回收完,恢復gc.get_count()的次數(0,0,0)
gc.get_count()返回0代建立物件的個數即當前已經發生回收的次數,比如(10,9,5)表示當前新建的物件10個,還沒有經過分代回收,而已經發生回收9*5=45次

python2沒有這個函式gc.get_stats()返回0、1、2代各自的(還未回收的數量,已經進行垃圾回收的次數,不可以回收的物件)
In [16]: gc.get_stats()
Out[16]:
[{'collected': 16564, 'collections': 333, 'uncollectable': 0},
 {'collected': 2373, 'collections': 27, 'uncollectable': 0},
 {'collected': 119, 'collections': 10, 'uncollectable': 0}]


執行一次gc.collect(0),則gc.get_count()的第2個值即1代的值加1,表示進行了一次0代回收
執行一次gc.collect(1),則gc.get_count()的第3個值即2代的值加1,同時第2個值即1代的值變為0,表示進行了一次1代回收
執行一次gc.collect(2),則gc.get_count()的第3個值即2代的值變為0,表示進行了一次2代回收


小物件:int、str並不會建立物件,所有使用list,dict、tuple等;str/int有個地址池,這類小物件的建立不體現在計數裡,所以gc.get_count()[0]裡體現不出變化
驗證的時候使用python3.7的編譯環境,不要使用ipython這種環境

gc.set_threshold正確理解,預設值[700,10,10]:7000*10*10=70000

開啟debug和gc.enable():gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_COLLECTABLE)

示例:
執行下面的的語句,建立10W個物件,你會發現有100+次0代垃圾回收,10+次1代回收,1次2代垃圾回收,證明:是根據頻次來計算的
以python3測試為主:
python裡的批量建立物件的方法:locals()

for i in range(100000):
  locals()['str'+str(i)]={'a':i}

或者稍微大點的物件:
for i in range(100000):
  locals()['strwo'+str(i)]={'a':{'b':str(i)},'xx':{'wo':str(i)+'0代的700表示新建立的物件超過700則對0代進行垃圾回收,能回收的回收   ,不能回收的放到1代中。1代的預設閾值10表示記錄0代進行回收的次數,即0代回收次數超過10次,則對1代進'}}

參考:
https://foofish.net/python-gc.html
https://juejin.im/post/5b34b117f265da59a50b2fbe