python垃圾回收价格表_深度解析Python垃圾回收机制(超级详细)
我們知道,目前的計(jì)算機(jī)都采用的是圖靈機(jī)架構(gòu),其本質(zhì)就是用一條無限長的紙帶,對應(yīng)今天的存儲(chǔ)器。隨后在工程學(xué)的推演中,逐漸出現(xiàn)了寄存器、易失性存儲(chǔ)器(內(nèi)存)以及永久性存儲(chǔ)器(硬盤)等產(chǎn)品。由于不同的存儲(chǔ)器,其速度越快,單位價(jià)格也就越昂貴,因此,妥善利用好每一寸告訴存儲(chǔ)器的空間,永遠(yuǎn)是系統(tǒng)設(shè)計(jì)的一個(gè)核心。
Python?程序在運(yùn)行時(shí),需要在內(nèi)存中開辟出一塊空間,用于存放運(yùn)行時(shí)產(chǎn)生的臨時(shí)變量,計(jì)算完成后,再將結(jié)果輸出到永久性存儲(chǔ)器中。但是當(dāng)數(shù)據(jù)量過大,或者內(nèi)存空間管理不善,就很容易出現(xiàn)內(nèi)存溢出的情況,程序可能會(huì)被操作系統(tǒng)終止。
而對于服務(wù)器這種用于永不中斷的系統(tǒng)來說,內(nèi)存管理就顯得更為重要了,不然很容易引發(fā)內(nèi)存泄漏。
這里的內(nèi)存泄漏是指程序本身沒有設(shè)計(jì)好,導(dǎo)致程序未能釋放已不再使用的內(nèi)存,或者直接失去了對某段內(nèi)存的控制,造成了內(nèi)存的浪費(fèi)。
那么,對于不會(huì)再用到的內(nèi)存空間,Python 是通過什么機(jī)制來管理的呢?其實(shí)在前面章節(jié)已大致接觸過,就是引用計(jì)數(shù)機(jī)制。
Python引用計(jì)數(shù)機(jī)制
在學(xué)習(xí) Python 的整個(gè)過程中,我們一直在強(qiáng)調(diào),Python 中一切皆對象,也就是說,在 Python 中你用到的一切變量,本質(zhì)上都是類對象。
那么,如何知道一個(gè)對象永遠(yuǎn)都不能再使用了呢?很簡單,就是當(dāng)這個(gè)對象的引用計(jì)數(shù)值為 0 時(shí),說明這個(gè)對象永不再用,自然它就變成了垃圾,需要被回收。
舉個(gè)例子:
import os
import psutil
# 顯示當(dāng)前 python 程序占用的內(nèi)存大小
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')
輸出結(jié)果為:
initial memory used: 47.19140625 MB
after a created memory used: 433.91015625 MB
finished memory used: 48.109375 MB
注意,運(yùn)行此程序之前,需安裝 psutil 模塊(獲取系統(tǒng)信息的模塊),可使用 pip 命令直接安裝,執(zhí)行命令為 $pip install psutil,如果遇到 Permission denied 安裝失敗,請加上 sudo 重試。
可以看到,當(dāng)調(diào)用函數(shù) func() 且列表 a 被創(chuàng)建之后,內(nèi)存占用迅速增加到了 433 MB,而在函數(shù)調(diào)用結(jié)束后,內(nèi)存則返回正常。這是因?yàn)?#xff0c;函數(shù)內(nèi)部聲明的列表 a 是局部變量,在函數(shù)返回后,局部變量的引用會(huì)注銷掉,此時(shí)列表 a 所指代對象的引用計(jì)數(shù)為 0,Python 便會(huì)執(zhí)行垃圾回收,因此之前占用的大量內(nèi)存就又回來了。
明白了這個(gè)原理后,稍微修改上面的代碼,如下所示:
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')
輸出結(jié)果為:
initial memory used: 48.88671875 MB
after a created memory used: 433.94921875 MB
finished memory used: 433.94921875 MB
上面這段代碼中,global a 表示將 a 聲明為全局變量,則即使函數(shù)返回后,列表的引用依然存在,于是 a 對象就不會(huì)被當(dāng)做垃圾回收掉,依然占用大量內(nèi)存。
同樣,如果把生成的列表返回,然后在主程序中接收,那么引用依然存在,垃圾回收也不會(huì)被觸發(fā),大量內(nèi)存仍然被占用著:
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')
輸出結(jié)果為:
initial memory used: 47.96484375 MB
after a created memory used: 434.515625 MB
finished memory used: 434.515625 MB
以上最常見的幾種情況,下面由表及里,深入看一下 Python 內(nèi)部的引用計(jì)數(shù)機(jī)制。先來分析一段代碼:
import sys
a = []
# 兩次引用,一次來自 a,一次來自 getrefcount
print(sys.getrefcount(a))
def func(a):
# 四次引用,a,python 的函數(shù)調(diào)用棧,函數(shù)參數(shù),和 getrefcount
print(sys.getrefcount(a))
func(a)
# 兩次引用,一次來自 a,一次來自 getrefcount,函數(shù) func 調(diào)用已經(jīng)不存在
print(sys.getrefcount(a))
輸出結(jié)果為:
2
4
2
注意,sys.getrefcount() 函數(shù)用于查看一個(gè)變量的引用次數(shù),不過別忘了,getrefcount 本身也會(huì)引入一次計(jì)數(shù)。
另一個(gè)要注意的是,在函數(shù)調(diào)用發(fā)生的時(shí)候,會(huì)產(chǎn)生額外的兩次引用,一次來自函數(shù)棧,另一個(gè)是函數(shù)參數(shù)。
import sys
a = []
print(sys.getrefcount(a)) # 兩次
b = a
print(sys.getrefcount(a)) # 三次
c = b
d = b
e = c
f = e
g = d
print(sys.getrefcount(a)) # 八次
輸出結(jié)果為:
2
3
8
分析一下這段代碼,a、b、c、d、e、f、g 這些變量全部指代的是同一個(gè)對象,而 sys.getrefcount() 函數(shù)并不是統(tǒng)計(jì)一個(gè)指針,而是要統(tǒng)計(jì)一個(gè)對象被引用的次數(shù),所以最后一共會(huì)有 8 次引用。
理解引用這個(gè)概念后,引用釋放是一種非常自然和清晰的思想。相比 C 語言中需要使用 free 去手動(dòng)釋放內(nèi)存,Python 的垃圾回收在這里可以說是省心省力了。
不過,有讀者還是會(huì)好奇,如果想手動(dòng)釋放內(nèi)存,應(yīng)該怎么做呢?方法同樣很簡單,只需要先調(diào)用 del a 來刪除一個(gè)對象,然后強(qiáng)制調(diào)用 gc.collect() 即可手動(dòng)啟動(dòng)垃圾回收。例如:
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)
輸出結(jié)果為:
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)
in
11
12 show_memory_info('finish')
---> 13 print(a)
NameError: name 'a' is not defined
是不是覺得垃圾回收非常簡單呢?這里再問大家一個(gè)問題:引用次數(shù)為 0 是垃圾回收啟動(dòng)的充要條件嗎?還有沒有其他可能性呢?
其實(shí),引用計(jì)數(shù)是其中最簡單的實(shí)現(xiàn),引用計(jì)數(shù)并非充要條件,它只能算作充分非必要條件,至于其他的可能性,下面所講的循環(huán)引用正是其中一種。
循環(huán)引用
首先思考一個(gè)問題,如果有兩個(gè)對象,之間互相引用,且不再被別的對象所引用,那么它們應(yīng)該被垃圾回收嗎?
舉個(gè)例子:
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')
輸出結(jié)果為:
initial memory used: 47.984375 MB
after a, b created memory used: 822.73828125 MB
finished memory used: 821.73046875 MB
程序中,a 和 b 互相引用,并且作為局部變量在函數(shù) func 調(diào)用結(jié)束后,a 和 b 這兩個(gè)指針從程序意義上已經(jīng)不存在,但從輸出結(jié)果中看到,依然有內(nèi)存占用,這是為什么呢?因?yàn)榛ハ嘁脤?dǎo)致它們的引用數(shù)都不為 0。
試想一下,如果這段代碼出現(xiàn)在生產(chǎn)環(huán)境中,哪怕 a 和 b 一開始占用的空間不是很大,但經(jīng)過長時(shí)間運(yùn)行后,Python 所占用的內(nèi)存一定會(huì)變得越來越大,最終撐爆服務(wù)器,后果不堪設(shè)想。
有讀者可能會(huì)說,互相引用還是很容易被發(fā)現(xiàn)的呀,問題不大。可是,更隱蔽的情況是出現(xiàn)一個(gè)引用環(huán),在工程代碼比較復(fù)雜的情況下,引用環(huán)真不一定能被輕易發(fā)現(xiàn)。那么應(yīng)該怎么做呢?
事實(shí)上,Python 本身能夠處理這種情況,前面剛剛講過,可以顯式調(diào)用 gc.collect() 來啟動(dòng)垃圾回收,例如:
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')
輸出結(jié)果為:
initial memory used: 49.51171875 MB
after a, b created memory used: 824.1328125 MB
finished memory used: 49.98046875 MB
事實(shí)上,Python 使用標(biāo)記清除(mark-sweep)算法和分代收集(generational),來啟用針對循環(huán)引用的自動(dòng)垃圾回收。
先來看標(biāo)記清除算法。我們先用圖論來理解不可達(dá)的概念。對于一個(gè)有向圖,如果從一個(gè)節(jié)點(diǎn)出發(fā)進(jìn)行遍歷,并標(biāo)記其經(jīng)過的所有節(jié)點(diǎn);那么,在遍歷結(jié)束后,所有沒有被標(biāo)記的節(jié)點(diǎn),我們就稱之為不可達(dá)節(jié)點(diǎn)。顯而易見,這些節(jié)點(diǎn)的存在是沒有任何意義的,自然的,我們就需要對它們進(jìn)行垃圾回收。
當(dāng)然,每次都遍歷全圖,對于 Python 而言是一種巨大的性能浪費(fèi)。所以,在 Python 的垃圾回收實(shí)現(xiàn)中,標(biāo)記清除算法使用雙向鏈表維護(hù)了一個(gè)數(shù)據(jù)結(jié)構(gòu),并且只考慮容器類的對象(只有容器類對象才有可能產(chǎn)生循環(huán)引用)。
而分代收集算法,則是將 Python 中的所有對象分為三代。剛剛創(chuàng)立的對象是第 0 代;經(jīng)過一次垃圾回收后,依然存在的對象,便會(huì)依次從上一代挪到下一代。而每一代啟動(dòng)自動(dòng)垃圾回收的閾值,則是可以單獨(dú)指定的。當(dāng)垃圾回收器中新增對象減去刪除對象達(dá)到相應(yīng)的閾值時(shí),就會(huì)對這一代對象啟動(dòng)垃圾回收。
事實(shí)上,分代收集基于的思想是,新生的對象更有可能被垃圾回收,而存活更久的對象也有更高的概率繼續(xù)存活。因此,通過這種做法,可以節(jié)約不少計(jì)算量,從而提高 Python 的性能。
總結(jié)
以上是生活随笔為你收集整理的python垃圾回收价格表_深度解析Python垃圾回收机制(超级详细)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python安装pyinstaller出
- 下一篇: pro调用python libs_使用W