python學習9:Python 中的「垃圾」回收
- 2019 年 12 月 12 日
- 筆記
作為一個對測試有情懷的人,希望本公眾號的文章對大家有些許的幫助,測試不容易,也請大家多多幫忙推廣,讓越來越多的小夥伴能夠在這條道路上互相扶持,一條道走到黑……
前言
對於python來說,一切皆為對象,所有的變數賦值都遵循著對象引用機制。程式在運行的時候,需要在記憶體中開闢出一塊空間,用於存放運行時產生的臨時變數;計算完成後,再將結果輸出到永久性存儲器中。如果數據量過大,記憶體空間管理不善就很容易出現 OOM(out of memory),俗稱爆記憶體,程式可能被作業系統中止。
而對於伺服器,記憶體管理則顯得更為重要,不然很容易引發記憶體泄漏 - 這裡的泄漏,並不是說你的記憶體出現了資訊安全問題,被惡意程式利用了,而是指程式本身沒有設計好,導致程式未能釋放已不再使用的記憶體。- 記憶體泄漏也不是指你的記憶體在物理上消失了,而是意味著程式碼在分配了某段記憶體後,因為設計錯誤,失去了對這段記憶體的控制,從而造成了記憶體的浪費。也就是這塊記憶體脫離了gc的控制
計數引用
因為python中一切皆為對象,你所看到的一切變數,本質上都是對象的一個指針。
當一個對象不再調用的時候,也就是當這個對象的引用計數(指針數)為 0 的時候,說明這個對象永不可達,自然它也就成為了垃圾,需要被回收。可以簡單的理解為沒有任何變數再指向它。
import os import psutil # 顯示當前 python 程式佔用的記憶體大小 def show_memory_info(hint): pid = os.getpid() p = psutil.Process(pid) info = p.memory_full_info() memory = info.uss / 1024. / 1024 print('{} memory used: {} MB'.format(hint, memory)) def func(): show_memory_info('initial') a = [i for i in range(10000000)] show_memory_info('after a created') func() show_memory_info('finished') ########## 輸出 ########## initial memory used: 47.19140625 MB after a created memory used: 433.91015625 MB finished memory used: 48.109375 MB可以看到調用函數 func(),在列表 a 被創建之後,記憶體佔用迅速增加到了 433 MB:而在函數調用結束後,記憶體則返回正常。這是因為,函數內部聲明的列表 a 是局部變數,在函數返回後,局部變數的引用會註銷掉;此時,列表 a 所指代對象的引用數為 0,Python 便會執行垃圾回收,因此之前佔用的大量記憶體就又回來了。
def func(): show_memory_info('initial') global a a = [i for i in range(10000000)] show_memory_info('after a created') func() show_memory_info('finished') ########## 輸出 ########## initial memory used: 48.88671875 MB after a created memory used: 433.94921875 MB finished memory used: 433.94921875 MB新的這段程式碼中,global a 表示將 a 聲明為全局變數。那麼,即使函數返回後,列表的引用依然存在,於是對象就不會被垃圾回收掉,依然佔用大量記憶體。同樣,如果我們把生成的列表返回,然後在主程式中接收,那麼引用依然存在,垃圾回收就不會被觸發,大量記憶體仍然被佔用著:
def func(): show_memory_info('initial') a = [i for i in derange(10000000)] show_memory_info('after a created') return a a = func() show_memory_info('finished') ########## 輸出 ########## initial memory used: 47.96484375 MB after a created memory used: 434.515625 MB finished memory used: 434.515625 MB那怎麼可以看到變數被引用了多少次呢?通過 sys.getrefcount
import sys a = [] # 兩次引用,一次來自 a,一次來自 getrefcount print(sys.getrefcount(a)) def func(a): # 四次引用,a,python 的函數調用棧,函數參數,和 getrefcount print(sys.getrefcount(a)) func(a) # 兩次引用,一次來自 a,一次來自 getrefcount,函數 func 調用已經不存在 print(sys.getrefcount(a)) ########## 輸出 ########## 2 4 2
如果其中涉及函數調用,會額外增加兩次 1. 函數棧 2. 函數調用
從這裡就可以看到python不再需要像C那種的認為的釋放記憶體,但是python同樣給我們提供了手動釋放記憶體的方法 gc.collect()
import gc show_memory_info('initial') a = [i for i in range(10000000)] show_memory_info('after a created') del a gc.collect() show_memory_info('finish') print(a) ########## 輸出 ########## initial memory used: 48.1015625 MB after a created memory used: 434.3828125 MB finish memory used: 48.33203125 MB --------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-12-153e15063d8a> in <module> 11 12 show_memory_info('finish') ---> 13 print(a) NameError: name 'a' is not defined截止目前,貌似python的垃圾回收機制非常的簡單,只要對象引用次數為0,必定為觸發gc,那麼引用次數為0是否是觸發gc的充要條件呢?
循環回收
如果有兩個對象,它們互相引用,並且不再被別的對象所引用,那麼它們應該被垃圾回收嗎?
def func(): show_memory_info('initial') a = [i for i in range(10000000)] b = [i for i in range(10000000)] show_memory_info('after a, b created') a.append(b) b.append(a) func() show_memory_info('finished') ########## 輸出 ########## initial memory used: 47.984375 MB after a, b created memory used: 822.73828125 MB finished memory used: 821.73046875 MB從結果顯而易見,它們並沒有被回收,但是從程式上來看,當這個函數結束的時候,作為局部變數的a,b就已經從程式意義上不存在了。但是因為它們的互相引用,導致了它們的引用數都不為0。
這時要如何規避呢 1. 從程式碼邏輯上進行整改,避免這種循環引用 2. 通過人工回收
import gc def func(): show_memory_info('initial') a = [i for i in range(10000000)] b = [i for i in range(10000000)] show_memory_info('after a, b created') a.append(b) b.append(a) func() gc.collect() show_memory_info('finished') ########## 輸出 ########## initial memory used: 49.51171875 MB after a, b created memory used: 824.1328125 MB finished memory used: 49.98046875 MBpython針對循環引用,有它的自動垃圾回收演算法 1. 標記清除(mark-sweep)演算法 2. 分代收集(generational)
標記清除
標記清除的步驟總結為如下步驟 1. GC會把所有的『活動對象』打上標記 2. 把那些沒有標記的對象『非活動對象』進行回收
那麼python如何判斷何為非活動對象?
通過用圖論來理解不可達的概念。對於一個有向圖,如果從一個節點出發進行遍歷,並標記其經過的所有節點;那麼,在遍歷結束後,所有沒有被標記的節點,我們就稱之為不可達節點。顯而易見,這些節點的存在是沒有任何意義的,自然的,我們就需要對它們進行垃圾回收。
但是每次都遍歷全圖,對於 Python 而言是一種巨大的性能浪費。所以,在 Python 的垃圾回收實現中,mark-sweep 使用雙向鏈表維護了一個數據結構,並且只考慮容器類的對象(只有容器類對象,list、dict、tuple,instance,才有可能產生循環引用)。

圖中把小黑圈視為全局變數,也就是把它作為root object,從小黑圈出發,對象1可直達,那麼它將被標記,對象2、3可間接到達也會被標記,而4和5不可達,那麼1、2、3就是活動對象,4和5是非活動對象會被GC回收。
分代回收
分代回收是一種以空間換時間的操作方式,Python將記憶體根據對象的存活時間劃分為不同的集合,每個集合稱為一個代,Python將記憶體分為了3「代」,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個鏈表,它們的垃圾收集頻率與對象的存活時間的增大而減小。新創建的對象都會分配在年輕代,年輕代鏈表的總數達到上限時(當垃圾回收器中新增對象減去刪除對象達到相應的閾值時),Python垃圾收集機制就會被觸發,把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活於整個系統的生命周期內。同時,分代回收是建立在標記清除技術基礎之上。
事實上,分代回收基於的思想是,新生的對象更有可能被垃圾回收,而存活更久的對象也有更高的概率繼續存活。因此,通過這種做法,可以節約不少計算量,從而提高 Python 的性能。
所以對於剛剛的問題,引用計數只是觸發gc的一個充分非必要條件,循環引用同樣也會觸發。
調試
可以使用 objgraph來調試程式,因為目前它的官方文檔,還沒有細讀,只能把文檔放在這供大家參閱啦~其中兩個函數非常有用 1. show_refs() 2. show_backrefs()
總結
- 垃圾回收是 Python 自帶的機制,用於自動釋放不會再用到的記憶體空間;
- 引用計數是其中最簡單的實現,不過切記,這只是充分非必要條件,因為循環引用需要通過不可達判定,來確定是否可以回收;
- Python 的自動回收演算法包括標記清除和分代回收,主要針對的是循環引用的垃圾收集;
- 調試記憶體泄漏方面, objgraph 是很好的可視化分析工具。
星雲測試
http://www.teststars.cc
奇林軟體
http://www.kylinpet.com
聯合通測
http://www.quicktesting.net

