Python学习:垃圾回收机制
Python 程序在運行的時候,需要在內存中開辟出一塊空間,用于存放運行時產生的臨時變量;計算完成后,再將結果輸出到永久性存儲器中。如果數據量過大,內存空間管理不善就很容易出現 OOM(out of memory),俗稱爆內存,程序可能被操作系統中止。
而對于服務器,這種設計為永不中斷的系統來說,內存管理則顯得更為重要,不然很容易引發內存泄漏。什么是內存泄漏呢?
那么,Python 又是怎么解決這些問題的?換句話說,對于不會再用到的內存空間,Python 是通過什么機制來回收這些空間的呢?
一、計數引用
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. / 1024print('{} 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 aa = [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 aa = 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這是最常見的幾種情況。下面,深入看一下 Python 內部的引用計數機制。先來看代碼:
import sys a = [] # 兩次引用,一次來自 a,一次來自 getrefcount print(sys.getrefcount(a))def func(a):# 四次引用,a,python 的函數調用棧,函數參數,和 getrefcountprint(sys.getrefcount(a)) func(a) # 兩次引用,一次來自 a,一次來自 getrefcount,函數 func 調用已經不存在 print(sys.getrefcount(a))########## 輸出 ##########2 4 2簡單介紹一下,sys.getrefcount() 這個函數,可以查看一個變量的引用次數。這段代碼本身應該很好理解,不過別忘了,getrefcount 本身也會引入一次計數。
另一個要注意的是,在函數調用發生的時候,會產生額外的兩次引用,一次來自函數棧,另一個是函數參數。
import sysa = []print(sys.getrefcount(a)) # 兩次 b = aprint(sys.getrefcount(a)) # 三次c = b d = b e = c f = e g = dprint(sys.getrefcount(a)) # 八次########## 輸出 ##########2 3 8看到這段代碼,需要稍微注意一下,a、b、c、d、e、f、g 這些變量全部指代的是同一個對象,而 sys.getrefcount() 函數并不是統計一個指針,而是要統計一個對象被引用的次數,所以最后一共會有八次引用。
理解引用這個概念后,引用釋放是一種非常自然和清晰的思想。相比 C 語言里,你需要使用 free 去手動釋放內存,Python 的垃圾回收在這里可以說是省心省力了。
Python想手動釋放內存,應該怎么做呢?
方法同樣很簡單。只需要先調用 del a 來刪除一個對象;然后強制調用 gc.collect(),即可手動啟動垃圾回收。
import gcshow_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引用計數機制優點:
引用計數機制缺點:
那么,如果此時有面試官問:引用次數為 0 是垃圾回收啟動的充要條件嗎?還有沒有其他可能性呢?
二、循環引用
先來思考這么一個問題:如果有兩個對象,它們互相引用,并且不再被別的對象所引用,那么它們應該被垃圾回收嗎?
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 互相引用,并且,作為局部變量,在函數 func 調用結束后,a 和 b 這兩個指針從程序意義上已經不存在了。但是,很明顯,依然有內存占用!為什么呢?因為互相引用,導致它們的引用數都不為 0。
事實上,Python 本身能夠處理這種情況,剛剛提到過的,可以顯式調用 gc.collect() ,來啟動垃圾回收。
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 使用標記清除(mark-sweep)算法和分代收集(generational),來啟用針對循環引用的自動垃圾回收。
先來看標記清除算法。先用圖論來理解不可達的概念。對于一個有向圖,如果從一個節點出發進行遍歷,并標記其經過的所有節點;那么,在遍歷結束后,所有沒有被標記的節點,我們就稱之為不可達節點。顯而易見,這些節點的存在是沒有任何意義的,自然的,我們就需要對它們進行垃圾回收。
當然,每次都遍歷全圖,對于 Python 而言是一種巨大的性能浪費。所以,在 Python 的垃圾回收實現中,mark-sweep 使用雙向鏈表維護了一個數據結構,并且只考慮容器類的對象(只有容器類對象才有可能產生循環引用)。
而分代收集算法,則是另一個優化手段。Python 將所有對象分為三代。剛剛創立的對象是第 0 代;經過一次垃圾回收后,依然存在的對象,便會依次從上一代挪到下一代。而每一代啟動自動垃圾回收的閾值,則是可以單獨指定的。當垃圾回收器中新增對象減去刪除對象達到相應的閾值時,就會對這一代對象啟動垃圾回收。
事實上,分代收集基于的思想是,新生的對象更有可能被垃圾回收,而存活更久的對象也有更高的概率繼續存活。因此,通過這種做法,可以節約不少計算量,從而提高 Python 的性能。
三、調試內存泄漏
不過,雖然有了自動回收機制,但這也不是萬能的,難免還是會有漏網之魚。內存泄漏是不想見到的,而且還會嚴重影響性能。有沒有什么好的調試手段呢?
objgraph,一個非常好用的可視化引用關系的包。在這個包中,主要推薦兩個函數,第一個是 show_refs(),它可以生成清晰的引用關系圖。
通過下面這段代碼和生成的引用調用圖,能非常直觀地發現,有兩個 list 互相引用,說明這里極有可能引起內存泄露。這樣一來,再去代碼層排查就容易多了。
import objgrapha = [1, 2, 3] b = [4, 5, 6] a.append(b) b.append(a)objgraph.show_refs([a])
而另一個非常有用的函數,是 show_backrefs()。下面同樣為示例代碼和生成圖
相比剛才的引用調用圖,這張圖顯得稍微復雜一些。這個 API 有很多有用的參數,比如層數限制(max_depth)、寬度限制(too_many)、輸出格式控制(filename output)、節點過濾(filter, extra_ignore)等。
總結
參考:
《Python核心技術與實戰》
python垃圾回收機制(Garbage collection)
總結
以上是生活随笔為你收集整理的Python学习:垃圾回收机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MATLAB图像的频域低通滤波(灰度图像
- 下一篇: DHU Python Curriculu