一文解密Python中的垃圾回收
前言
我們知道,python
是一種高級(jí)編程語(yǔ)言,它提供了自動(dòng)內(nèi)存管理的功能,即垃圾回收機(jī)制。垃圾回收機(jī)制是一種自動(dòng)管理內(nèi)存的技術(shù),它可以幫助開(kāi)發(fā)者在編寫(xiě)代碼時(shí)不必關(guān)注內(nèi)存的分配和釋放,從而提高開(kāi)發(fā)效率。好奇的同學(xué)會(huì)問(wèn)了,python
的垃圾回收機(jī)制到底是如何實(shí)現(xiàn)的呢?帶著疑問(wèn),我們一起進(jìn)行探索。
為啥需要垃圾回收
- 內(nèi)存泄漏:在程序運(yùn)行過(guò)程中,如果開(kāi)發(fā)者沒(méi)有正確地釋放不再使用的內(nèi)存,就會(huì)導(dǎo)致內(nèi)存泄漏。內(nèi)存泄漏會(huì)導(dǎo)致程序占用的內(nèi)存越來(lái)越多,最終可能導(dǎo)致程序崩潰或者系統(tǒng)變得非常緩慢。垃圾回收機(jī)制可以自動(dòng)檢測(cè)和回收不再使用的內(nèi)存,避免內(nèi)存泄漏的問(wèn)題。
- 簡(jiǎn)化內(nèi)存管理:在一些低級(jí)編程語(yǔ)言中,開(kāi)發(fā)者需要手動(dòng)分配和釋放內(nèi)存,這樣容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問(wèn)題。而垃圾回收機(jī)制可以自動(dòng)管理內(nèi)存,開(kāi)發(fā)者不需要關(guān)注內(nèi)存的分配和釋放,從而提高開(kāi)發(fā)效率。
總之,垃圾回收的存在是為了解決內(nèi)存泄漏和簡(jiǎn)化內(nèi)存管理的問(wèn)題。它可以自動(dòng)檢測(cè)和回收不再使用的內(nèi)存,避免內(nèi)存泄漏,并提高開(kāi)發(fā)效率。在高級(jí)編程語(yǔ)言中,垃圾回收是一項(xiàng)非常重要的功能。
怎么實(shí)現(xiàn)的呢
Python 的垃圾回收機(jī)制主要通過(guò)引用計(jì)數(shù)和循環(huán)引用檢測(cè)來(lái)實(shí)現(xiàn)。
引用計(jì)數(shù)
引用計(jì)數(shù)是一種簡(jiǎn)單而高效的垃圾回收算法,它通過(guò)記錄每個(gè)對(duì)象的引用數(shù)量來(lái)判斷對(duì)象是否仍然被使用。當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為0時(shí),說(shuō)明該對(duì)象已經(jīng)不再被使用,可以被回收。接下來(lái),我們利用sys.getrefcount()
查看變量的引用次數(shù),這樣你一定會(huì)清晰很多。
案例一
import sys ? a = [] ? print(sys.getrefcount(a)) # 2 ? def func(a): ? ?print(sys.getrefcount(a)) # 4 ? func(a) ? print(sys.getrefcount(a)) # 2 ?
- 第一個(gè)
print
會(huì)輸出2,有2次引用,一次來(lái)自 a,一次來(lái)自 getrefcount - 第二個(gè)
print
會(huì)輸出4,有4次引用,一次來(lái)自a,一次來(lái)自python 的函數(shù)調(diào)用棧,一次來(lái)自函數(shù)參數(shù),一次來(lái)自 getrefcount - 第三個(gè)
print
會(huì)輸出2,有2次引用,一次來(lái)自a,一次來(lái)自 getrefcount
強(qiáng)調(diào)一點(diǎn):在函數(shù)調(diào)用發(fā)生時(shí),會(huì)產(chǎn)生額外的2次引用,一次來(lái)自函數(shù)棧,一次來(lái)自函數(shù)參數(shù)
案例二
我們?cè)谂e個(gè)例子,加深理解
import sys ? a = [] ? print(sys.getrefcount(a)) # 2 ? b = a ? print(sys.getrefcount(a)) # 3 ? c = b d = b e = c f = e g = d ? print(sys.getrefcount(a)) # 8 ?
可以看到a、b、c、d、e、f、g 這些變量全部指代的是同一個(gè)對(duì)象,這個(gè)對(duì)象被引用8次,所以最終輸出8
案例三
我們看看,未回收和回收后內(nèi)存的變化。
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('{} 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')
我們定義了一個(gè)函數(shù)show_memory_info
用來(lái)打印當(dāng)前python
程序占用的內(nèi)存大小,定義了一個(gè)函數(shù)func()
來(lái)創(chuàng)建變量a
,在創(chuàng)建變量a
之前打印占用內(nèi)存,最后在函數(shù)func()
調(diào)用銷毀后,再次打印內(nèi)存占用。在看過(guò)案例一之后,相信你一定知道,函數(shù)內(nèi)部聲明的列表 a 是局部變量,在函數(shù)返回后,局部變量的引用會(huì)注銷掉;此時(shí),列表 a 所指代對(duì)象的引用數(shù)為 0,Python 便會(huì)執(zhí)行垃圾回收。我們看看執(zhí)行結(jié)果是不是這樣:
initial memory used: 30.75 MB
after a created memory used: 415.6328125 MB
finished memory used: 30.98828125 MB
可以看到確實(shí)如此。
那我們?nèi)绻麑⒆兞柯暶鳛槿肿兞?,這樣函數(shù)銷毀后,列表的引用計(jì)數(shù)還存在,內(nèi)存應(yīng)該還是很大。測(cè)試一下:
def func(): ? ?show_memory_info('initial') ? ?global a ? ?a = [i for i in range(10000000)] ? ?show_memory_info('after a created')
執(zhí)行結(jié)果如下:
initial memory used: 30.25390625 MB
after a created memory used: 415.38671875 MB
finished memory used: 415.38671875 MB
可以看到結(jié)果是滿足預(yù)期的。
那如果我們將func()
函數(shù)生成的列表返回return a
,然后調(diào)用函數(shù)并賦值給一個(gè)變量,此時(shí)列表引用也會(huì)存在,內(nèi)存不會(huì)釋放。測(cè)試一下
def func(): ? ?show_memory_info('initial') ? ?a = [i for i in range(10000000)] ? ?show_memory_info('after a created') ? ?return a ? f = func()
執(zhí)行結(jié)果如下:
initial memory used: 30.1875 MB
after a created memory used: 415.0703125 MB
finished memory used: 415.0703125 MB
可以看到,確實(shí)還有大量?jī)?nèi)存被占用。
到這里,應(yīng)該對(duì)引用計(jì)數(shù)釋放內(nèi)存有一個(gè)清晰的認(rèn)識(shí)了吧,現(xiàn)在,有人會(huì)問(wèn),我確實(shí)在某種場(chǎng)景下,需要手動(dòng)釋放內(nèi)存該怎么辦呢?當(dāng)然python
也是支持的,還是上面定義全局變量a
的例子,我們只需要最后執(zhí)行del a
,刪除對(duì)象的引用,然后強(qiáng)制調(diào)用gc.collect()
,即可手動(dòng)啟動(dòng)垃圾回收。
循環(huán)引用
在上面案例三中,我們提到局部變量,在函數(shù)返回后,局部變量的引用會(huì)注銷掉??聪旅孢@段例子:
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')
按照我們上面學(xué)習(xí)的,a和b都是局部變量,函數(shù)返回后,應(yīng)該引用計(jì)數(shù)會(huì)變?yōu)?,內(nèi)存會(huì)釋放,我們測(cè)試一下:
執(zhí)行結(jié)果如下:
initial memory used: 30.80078125 MB
after a, b created memory used: 801.99609375 MB
finished memory used: 801.99609375 MB
可以看到內(nèi)存并沒(méi)有釋放,說(shuō)明a
和b
的引用計(jì)數(shù)應(yīng)該不為0。為啥出現(xiàn)這種情況呢?就是因?yàn)橄嗷ヒ?。那這種情況怎么解決呢?引用計(jì)數(shù)最后一部分提到,可以強(qiáng)制調(diào)用gc.collect()
什么是循環(huán)引用
循環(huán)引用是指兩個(gè)或多個(gè)對(duì)象之間相互引用,形成一個(gè)環(huán)狀結(jié)構(gòu)。這種情況下,引用計(jì)數(shù)算法無(wú)法正確判斷對(duì)象是否仍然被使用,因?yàn)樗鼈兊囊糜?jì)數(shù)永遠(yuǎn)不會(huì)變?yōu)?。為了解決循環(huán)引用的問(wèn)題,Python 引入了垃圾回收器,它使用了一種稱為標(biāo)記-清除的算法。
標(biāo)記-清除算法分為兩個(gè)階段:標(biāo)記階段和清除階段。在標(biāo)記階段,垃圾回收器會(huì)從根對(duì)象開(kāi)始遍歷所有可達(dá)對(duì)象,并將它們標(biāo)記為活動(dòng)對(duì)象。而在清除階段,垃圾回收器會(huì)遍歷整個(gè)堆內(nèi)存,將未標(biāo)記的對(duì)象進(jìn)行回收。
除了引用計(jì)數(shù)和標(biāo)記-清除算法,Python 還使用了分代回收的策略。分代回收是一種基于對(duì)象存活時(shí)間的優(yōu)化策略,它將對(duì)象分為不同的代,每個(gè)代有不同的回收頻率。一般來(lái)說(shuō),新創(chuàng)建的對(duì)象會(huì)被分配到第0代,而經(jīng)過(guò)一次垃圾回收后仍然存活的對(duì)象會(huì)被提升到下一代。這樣可以減少垃圾回收的頻率,提高性能。
如何調(diào)試內(nèi)存泄漏
這里推薦objgraph
,是一個(gè)可視化引用關(guān)系的包。
import objgraph ? a = [1, 2, 3] b = [4, 5, 6] ? a.append(b) b.append(a) ? objgraph.show_refs([a])
這里通過(guò)show_refs()
可以生成清晰的引用關(guān)系圖。
更多用法,可以參考文檔
最后
Python 的垃圾回收機(jī)制是一種自動(dòng)管理內(nèi)存的技術(shù),它通過(guò)引用計(jì)數(shù)和循環(huán)引用檢測(cè)來(lái)判斷對(duì)象是否仍然被使用,并使用標(biāo)記-清除算法進(jìn)行回收。此外,還采用了分代回收的策略來(lái)優(yōu)化性能。了解這些垃圾回收機(jī)制的工作原理對(duì)于編寫(xiě)高效的 Python 代碼非常重要。
以上就是一文解密Python中的垃圾回收的詳細(xì)內(nèi)容,更多關(guān)于Python垃圾回收的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python實(shí)現(xiàn)網(wǎng)頁(yè)自動(dòng)簽到功能
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)網(wǎng)頁(yè)自動(dòng)簽到功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01Python Requests模擬登錄實(shí)現(xiàn)圖書(shū)館座位自動(dòng)預(yù)約
這篇文章主要為大家詳細(xì)介紹了Python Requests的模擬登錄,Python實(shí)現(xiàn)圖書(shū)館座位自動(dòng)預(yù)約,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04python實(shí)現(xiàn)的系統(tǒng)實(shí)用log類實(shí)例
這篇文章主要介紹了python實(shí)現(xiàn)的系統(tǒng)實(shí)用log類,實(shí)例分析了Python基于logging模塊實(shí)現(xiàn)日志類的相關(guān)技巧,需要的朋友可以參考下2015-06-06python里的單引號(hào)和雙引號(hào)的有什么作用
在本篇文章里小編給大家分享的是一篇關(guān)于python里的單引號(hào)和雙引號(hào)的作用的相關(guān)內(nèi)容,需要的朋友們可以學(xué)習(xí)下。2020-06-06