什么是內(nèi)存管理器(what)
Python作為一個(gè)高層次的結(jié)合了解釋性、編譯性合溺、互動(dòng)性和面向?qū)ο蟮哪_本語言羹幸,與大多數(shù)編程語言不同,Python中的變量無需事先申明辫愉,變量無需指定類型栅受,程序員無需關(guān)心內(nèi)存管理,Python解釋器給你自動(dòng)回收恭朗。開發(fā)人員不用過多的關(guān)心內(nèi)存管理機(jī)制屏镊,這一切全部由python內(nèi)存管理器承擔(dān)了復(fù)雜的內(nèi)存管理工作。
內(nèi)存不外乎創(chuàng)建和銷毀兩部分痰腮,本文將圍繞python的內(nèi)存池和垃圾回收兩部分進(jìn)行分析而芥。
如果大家在學(xué)習(xí)中遇到困難,想找一個(gè)python學(xué)習(xí)交流環(huán)境膀值,可以加入我們的python裙棍丐,裙號(hào)930900780,可領(lǐng)取python學(xué)習(xí)資料沧踏,會(huì)節(jié)約很多時(shí)間歌逢,減少很多遇到的難題。
當(dāng)創(chuàng)建大量消耗小內(nèi)存的對(duì)象時(shí)翘狱,頻繁調(diào)用new/malloc會(huì)導(dǎo)致大量的內(nèi)存碎片秘案,致使效率降低。內(nèi)存池的作用就是預(yù)先在內(nèi)存中申請(qǐng)一定數(shù)量的潦匈,大小相等的內(nèi)存塊留作備用阱高,當(dāng)有新的內(nèi)存需求時(shí),就先從內(nèi)存池中分配內(nèi)存給這個(gè)需求茬缩,不夠之后再申請(qǐng)新的內(nèi)存赤惊。這樣做最顯著的優(yōu)勢(shì)就是能夠減少內(nèi)存碎片,提升效率凰锡。
python中的內(nèi)存管理機(jī)制為Pymalloc
首先未舟,我們看一張CPython(python解釋器)的內(nèi)存架構(gòu)圖:
python的對(duì)象管理主要位于Level+1~Level+3層
Level+3層:對(duì)于python內(nèi)置的對(duì)象(比如int,dict等)都有獨(dú)立的私有內(nèi)存池,對(duì)象之間的內(nèi)存池不共享寡夹,即int釋放的內(nèi)存处面,不會(huì)被分配給float使用
Level+2層:當(dāng)申請(qǐng)的內(nèi)存大小小于256KB時(shí)厂置,內(nèi)存分配主要由 Python 對(duì)象分配器(Python’s object allocator)實(shí)施
Level+1層:當(dāng)申請(qǐng)的內(nèi)存大小大于256KB時(shí)菩掏,由Python原生的內(nèi)存分配器進(jìn)行分配,本質(zhì)上是調(diào)用C標(biāo)準(zhǔn)庫中的malloc/realloc等函數(shù)
關(guān)于釋放內(nèi)存方面昵济,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)變?yōu)?時(shí)智绸,Python就會(huì)調(diào)用它的析構(gòu)函數(shù)野揪。調(diào)用析構(gòu)函數(shù)并不意味著最終一定會(huì)調(diào)用free來釋放內(nèi)存空間,如果真是這樣的話瞧栗,那頻繁地申請(qǐng)、釋放內(nèi)存空間會(huì)使Python的執(zhí)行效率大打折扣。因此在析構(gòu)時(shí)也采用了內(nèi)存池機(jī)制驳棱,從內(nèi)存池申請(qǐng)到的內(nèi)存會(huì)被歸還到內(nèi)存池中塘安,以避免頻繁地申請(qǐng)和釋放動(dòng)作。
Python的垃圾回收機(jī)制采用引用計(jì)數(shù)機(jī)制為主殴边,標(biāo)記-清除和分代回收機(jī)制為輔的策略憎茂。其中,標(biāo)記-清除機(jī)制用來解決計(jì)數(shù)引用帶來的循環(huán)引用而無法釋放內(nèi)存的問題锤岸,分代回收機(jī)制是為提升垃圾回收的效率竖幔。
Python通過引用計(jì)數(shù)來保存內(nèi)存中的變量追蹤,即記錄該對(duì)象被其他使用的對(duì)象引用的次數(shù)是偷。
Python中有個(gè)內(nèi)部跟蹤變量叫做引用計(jì)數(shù)器拳氢,每個(gè)變量有多少個(gè)引用,簡稱引用計(jì)數(shù)蛋铆。當(dāng)某個(gè)對(duì)象的引用計(jì)數(shù)為0時(shí)馋评,就列入了垃圾回收隊(duì)列。
>>>a=[1,2]
>>>importsys
>>>sys.getrefcount(a)## 獲取對(duì)象a的引用次數(shù)
2
>>>b=a
>>>sys.getrefcount(a)
3
>>>delb## 刪除b的引用
>>>sys.getrefcount(a)
2
>>>c=list()
>>>c.append(a)## 加入到容器中
>>>sys.getrefcount(a)
3
>>>delc## 刪除容器刺啦,引用-1
>>>sys.getrefcount(a)
2
>>>b=a
>>>sys.getrefcount(a)
3
>>>a=[3,4]## 重新賦值
>>>sys.getrefcount(a)
2
復(fù)制代碼
注意:當(dāng)把a(bǔ)作為參數(shù)傳遞給getrefcount時(shí)栗恩,會(huì)產(chǎn)生一個(gè)臨時(shí)的引用,因此得出來的結(jié)果比真實(shí)情況+1
引用計(jì)數(shù)增加的情況:
一個(gè)對(duì)象被分配給一個(gè)新的名字(例如:a=[1,2])
將其放入一個(gè)容器中(如列表洪燥、元組或字典)(例如:c.append(a))
引用計(jì)數(shù)減少的情況:
使用del語句對(duì)對(duì)象別名顯式的銷毀(例如:del b)
對(duì)象所在的容器被銷毀或從容器中刪除對(duì)象(例如:del c )
引用超出作用域或被重新賦值(例如:a=[3,4])
引用計(jì)數(shù)能夠解決大多數(shù)垃圾回收的問題磕秤,但是遇到兩個(gè)對(duì)象相互引用的情況,del語句可以減少引用次數(shù)捧韵,但是引用計(jì)數(shù)不會(huì)歸0市咆,對(duì)象也就不會(huì)被銷毀,從而造成了內(nèi)存泄漏問題再来。針對(duì)該情況蒙兰,Python引入了標(biāo)記-清除機(jī)制。
標(biāo)記-清除用來解決引用計(jì)數(shù)機(jī)制產(chǎn)生的循環(huán)引用芒篷,進(jìn)而導(dǎo)致內(nèi)存泄漏的問題 搜变。 循環(huán)引用只有在容器對(duì)象才會(huì)產(chǎn)生,比如字典针炉,元組挠他,列表等。
顧名思義篡帕,該機(jī)制在進(jìn)行垃圾回收時(shí)分成了兩步殖侵,分別是:
標(biāo)記階段贸呢,遍歷所有的對(duì)象,如果是可達(dá)的(reachable)拢军,也就是還有對(duì)象引用它楞陷,那么就標(biāo)記該對(duì)象為可達(dá)
清除階段,再次遍歷對(duì)象茉唉,如果發(fā)現(xiàn)某個(gè)對(duì)象沒有標(biāo)記為可達(dá)(即為Unreachable)固蛾,則就將其回收
>>>a=[1,2]
>>>b=[3,4]
>>>sys.getrefcount(a)2
>>>sys.getrefcount(b)2
>>>a.append(b)>>> sys.getrefcount(b)3
>>>b.append(a)>>> sys.getrefcount(a)3
>>>dela
>>>delb
復(fù)制代碼
a引用b,b引用a,此時(shí)兩個(gè)對(duì)象各自被引用了2次(去除getrefcout()的臨時(shí)引用)
執(zhí)行del之后,對(duì)象a,b的引用次數(shù)都-1度陆,此時(shí)各自的引用計(jì)數(shù)器都為1魏铅,陷入循環(huán)引用
標(biāo)記:找到其中的一端a,因?yàn)樗幸粋€(gè)對(duì)b的引用,則將b的引用計(jì)數(shù)-1
標(biāo)記:再沿著引用到b,b有一個(gè)a的引用,將a的引用計(jì)數(shù)-1坚芜,此時(shí)對(duì)象a和b的引用次數(shù)全部為0览芳,被標(biāo)記為不可達(dá)(Unreachable)
清除: 被標(biāo)記為不可達(dá)的對(duì)象就是真正需要被釋放的對(duì)象
上面描述的垃圾回收的階段,會(huì)暫停整個(gè)應(yīng)用程序鸿竖,等待標(biāo)記清除結(jié)束后才會(huì)恢復(fù)應(yīng)用程序的運(yùn)行沧竟。為了減少應(yīng)用程序暫停的時(shí)間,Python 通過“分代回收”(Generational Collection)以空間換時(shí)間的方法提高垃圾回收效率缚忧。
分代回收是基于這樣的一個(gè)統(tǒng)計(jì)事實(shí)悟泵,對(duì)于程序,存在一定比例的內(nèi)存塊的生存周期比較短闪水;而剩下的內(nèi)存塊糕非,生存周期會(huì)比較長,甚至?xí)某绦蜷_始一直持續(xù)到程序結(jié)束球榆。生存期較短對(duì)象的比例通常在 80%~90%之間朽肥。 因此,簡單地認(rèn)為:對(duì)象存在時(shí)間越長持钉,越可能不是垃圾衡招,應(yīng)該越少去收集。這樣在執(zhí)行標(biāo)記-清除算法時(shí)可以有效減小遍歷的對(duì)象數(shù)每强,從而提高垃圾回收的速度始腾,是一種以空間換時(shí)間的方法策略。
Python將所有的對(duì)象分為年輕代(第0代)空执、中年代(第1代)浪箭、老年代(第2代)三代。所有的新建對(duì)象默認(rèn)是 第0代對(duì)象辨绊。當(dāng)在第0代的gc掃描中存活下來的對(duì)象將被移至第1代奶栖,在第1代的gc掃描中存活下來的對(duì)象將被移至第2代。
gc掃描次數(shù)(第0代>第1代>第2代)
當(dāng)某一代中被分配的對(duì)象與被釋放的對(duì)象之差達(dá)到某一閾值時(shí),就會(huì)觸發(fā)當(dāng)前一代的gc掃描驼抹。當(dāng)某一代被掃描時(shí)桑孩,比它年輕的一代也會(huì)被掃描拜鹤,因此框冀,第2代的gc掃描發(fā)生時(shí),第0敏簿,1代的gc掃描也會(huì)發(fā)生明也,即為全代掃描。
>>>importgc
>>>gc.get_threshold()## 分代回收機(jī)制的參數(shù)閾值設(shè)置
(700, 10, 10)
復(fù)制代碼
700=新分配的對(duì)象數(shù)量-釋放的對(duì)象數(shù)量惯裕,第0代gc掃描被觸發(fā)
第一個(gè)10:第0代gc掃描發(fā)生10次温数,則第1代的gc掃描被觸發(fā)
第二個(gè)10:第1代的gc掃描發(fā)生10次,則第2代的gc掃描被觸發(fā)
在標(biāo)記-清除中蜻势,如果對(duì)象c也引用a,執(zhí)行del操作后撑刺,會(huì)發(fā)生什么?
>>> a=[1,2]
>>> b=[3,4]
>>> c=a>>> a.append(b)
>>> b.append(a)
復(fù)制代碼
ref_count表示引用計(jì)數(shù)
對(duì)象a,b,c全部為reachable
執(zhí)行del之后握玛,引用關(guān)系如下圖所示:
>>>dela
>>>delb
復(fù)制代碼
a,b,c的ref_count減1
標(biāo)記: a引用b,將b的ref_count減1到0够傍,b引用a,將a的ref_count減1到1,將b放在unreachable下
再循環(huán):因?yàn)閍是可達(dá)的挠铲,所以會(huì)遞歸地將從a節(jié)點(diǎn)出發(fā)可以達(dá)到的所有節(jié)點(diǎn)標(biāo)記為reachable下冕屯,即為:
清除:unreachable下沒有可清除的對(duì)象,因此a,b,c對(duì)象不會(huì)被清除
總體而言拂苹,python通過內(nèi)存池來減少內(nèi)存碎片化安聘,提高執(zhí)行效率。主要通過引用計(jì)數(shù)來完成垃圾回收瓢棒,通過標(biāo)記-清除解決容器對(duì)象循環(huán)引用造成的問題浴韭,通過分代回收提高垃圾回收的效率。
最后多說一句脯宿,小編是一名python開發(fā)工程師囱桨,這里有我自己整理了一套最新的python系統(tǒng)學(xué)習(xí)教程,包括從基礎(chǔ)的python腳本到web開發(fā)嗅绰、爬蟲舍肠、數(shù)據(jù)分析、數(shù)據(jù)可視化窘面、機(jī)器學(xué)習(xí)等翠语。想要這些資料的可以進(jìn)裙930900780領(lǐng)取。
本文章素材來源于網(wǎng)絡(luò)财边,如有侵權(quán)請(qǐng)聯(lián)系刪除肌括。