1. 程式人生 > 其它 >python 基礎知識梳理——Python中的垃圾回收機制

python 基礎知識梳理——Python中的垃圾回收機制

技術標籤:Python學習python指標連結串列列表垃圾回收

python 基礎知識梳理——Python中的垃圾回收機制


1. 引言

當Python程式在執行的時候,需要在記憶體中開闢出一塊空間,用來存放執行時產生的臨時變數;計算完成後,再將結果輸出到永久性儲存器中。如果資料量過大,記憶體空間就有可能會出現OOM(out of memory),程式可能被作業系統終止。

洩漏指的是:程式本身沒有設計好,導致程式本身未能釋放已經不再使用的記憶體

記憶體洩漏指的是:在程式碼分配某段記憶體後,因為設計錯誤,失去了對這段記憶體的控制,從而造成的記憶體的浪費。

我們只需要記住最關鍵的一句話:

Python中的垃圾回收機制是以引用計數為主,標記-清除和分代回收兩種機制為輔的策略

2. 引用計數

Python中一切皆是物件,所以我們看到的所有的變數,本質上都是物件的一個指標。

那麼,我們怎麼判斷這個物件是否需要被回收呢?

當這個物件的引用計數(指標數)為0,那麼就意味著沒有對於這個物件的引用,自然這個物件就成了垃圾,需要被回收。

2.1 例1:a為區域性變數

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('{} memroy used:{} MB'.format(hint,memory)) def func(): show_memory_info('initial') a = [i for i in range(10000000)] show_memory_info('after created ') func() show_memory_info('finished') # 輸出 initial memroy used:
6.1796875 MB after created memroy used:398.26953125 MB finished memroy used:10.515625 MB

在呼叫func()後,在列表a建立後,記憶體佔用達到了近400MB,而在函式呼叫結束後,記憶體則恢復到了之前的水平。

函式內部宣告的列表a是區域性變數,在函式返回後,區域性變數的引用就會登出,此時,列表a所指代物件的引用數為0,Python便會執行垃圾回收,因此之前佔用的大量記憶體就被釋放了。

2.2 例2:a為全域性變數

那麼,我們將a宣告為全域性變數會發生什麼呢?

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('{} memroy used:{} MB'.format(hint,memory))

def func():
    show_memory_info('initial')
    global a # 宣告全域性變數a
    a = [i for i in range(10000000)]
    show_memory_info('after created ')

func()
show_memory_info('finished')
# 輸出
initial memroy used:6.16796875 MB
after created  memroy used:398.28515625 MB
finished memroy used:398.28515625 MB

在我們宣告a為全域性變數後,即使函式返回後,列表的引用依然存在,此時Python的垃圾回收機制就不會回收a,依然佔用記憶體。

2.3 例子3:a作為返回值

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('{} memroy used:{} MB'.format(hint,memory))

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after created ')
    return a

a = func()
show_memory_info('finished')
# 輸出
initial memroy used:6.1640625 MB
after created  memroy used:398.2578125 MB
finished memroy used:398.2578125 MB

a作為返回值的話,因為在a=fun()中引用了a,所以a還是不會被垃圾回收機制回收,記憶體仍然被佔用著。

2.4 引用計數原理

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

sys.getrefcount()這個函式,可以檢視一個變數的引用次數。

注意:getrefcount() 本身也會引用一次計數。

注意:當函式發生的時候,會產生額外兩次的引用,一次來自函式棧,另一次來自函式引數。

import sys
a = []
# 兩次引用計數,一次是a,另一次是sys.getrefcount()
print('引用計數為:{} 次'.format(sys.getrefcount(a)))

b = a
# 三次 引用計數,前兩次 + b=a的一次引用,sys.getrefcount()重複只計算一次
print('引用計數為:{} 次'.format(sys.getrefcount(a)))

c = b # 四次
d = b # 五次
e = b # 六次
f = b # 七次
g = d # 八次,通過 d 引用了 b
print('引用計數為:{} 次'.format(sys.getrefcount(a)))

2.5 手動垃圾回收

Python中垃圾回收相比於C語言中的free來釋放記憶體,簡單了很多,但是如果我們需要手動釋放記憶體呢?該如何操作呢?

那麼,我們就需要先呼叫del a來刪除一個物件,然後呼叫gc.collect()啟動垃圾回收,程式碼如下:

import gc
import os
import psutil

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memroy used:{} MB'.format(hint, memory))

show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after created')
del a
gc.collect()
show_memory_info('finished')
print(a)
# 輸出
initial memroy used:6.16015625 MB
after created memroy used:398.21875 MB
finished memroy used:10.46875 MB 
Traceback (most recent call last):
  File "/Users/gray/Desktop/test.py", line 18, in <module>
    print(a)
NameError: name 'a' is not defined

可見,這就是Python的手動回收機制,經過手動回收,記憶體空間被回收了,列表a也被刪除,所以報錯顯示a沒有定義。

3. 迴圈引用

我們知道了引用計數為0,Python就會回收,那麼如果兩個物件互相引用,那麼它們會被垃圾回收嗎?

我們從一個例子入手:

import gc
import os
import psutil

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memroy used:{} MB'.format(hint, memory))

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 memroy used:6.21484375 MB
after a,b created memroy used:660.42578125 MB
finished memroy used:660.15625 MB

很明顯,由於a和b之間的互相引用,即使a,b均為區域性變數,在函式呼叫結束之後,a和b的指標在程式意義上都不存在了,但是記憶體依然佔用。

在這樣簡單的程式碼中,我們還是能夠發現迴圈引用的,但是當工程程式碼複雜後,引用環不一定會容易被輕易發現。

迴圈引用這種情況,Python也是可以處理的,我們還是呼叫gc.collect()來手動啟動垃圾回收。

import gc
import os
import psutil

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memroy used:{} MB'.format(hint, memory))

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 memroy used:6.15234375 MB
after a,b created memroy used:784.7109375 MB
finished memroy used:10.74609375 MB

手動垃圾回收生效了,Python中的垃圾回收並沒有那麼弱。

4. Python的垃圾回收

針對迴圈引用,Python中有專門的標記-清除分代回收來處理。

4.1 標記-清除

對於一個有向圖,如果從一個節點出發進行遍歷,並標記其經過的所有節點;那麼在遍歷結束後,所有沒有被標記的節點,我們就稱之為不可達節點;顯而易見,這些節點的存在是沒有任何意義的,自然的,我們就需要對它們進行垃圾回收。

當然,每次都遍歷全圖,對於Python來說也是一種巨大的效能浪費,所以Python中的垃圾回收實現中,mark-sweep使用雙向連結串列維護了一個數據結構,並且只考慮容器類的物件(只有容器類物件才能產生迴圈引用)。

4.2 分代回收

Python將所有物件分為三代。剛剛建立的物件為0代;經歷一次垃圾回收後,依然存在的物件,便會依次從上一代挪到下一代;每一代啟動垃圾回收的閾值,是可以單獨設定的。當垃圾回收容器中新增物件減去刪除物件達到相應的閾值的時候,就會對這一代物件啟動垃圾回收。

分代回收的思想其實是:新生代物件更有可能會被回收,而存活更久的物件也有更高的概率繼續存活,通過這樣的思路,可以節省不少計算量,從而提高Python的效能。






博文的後續更新,請關注我的個人部落格:星塵部落格