Python 是如何管理內(nèi)存的?
在 GitHub 看到一篇很不錯(cuò)的學(xué)習(xí)資料,其中提到 Python 是如何管理內(nèi)存的,我看完后很有收獲,如下:
原文[1]
當(dāng)面試官問(wèn)到這個(gè)問(wèn)題的時(shí)候,一個(gè)展示自己的機(jī)會(huì)就擺在面前了。你要先反問(wèn)面試官:“你說(shuō)的是官方的CPython解釋器嗎?”。這個(gè)反問(wèn)可以展示出你了解過(guò) Python 解釋器的不同的實(shí)現(xiàn)版本,而且你也知道面試官想問(wèn)的是 CPython。當(dāng)然,很多面試官對(duì)不同的 Python 解釋器底層實(shí)現(xiàn)到底有什么差別也沒(méi)有概念。所以,千萬(wàn)不要覺(jué)得面試官一定比你強(qiáng),懷揣著這份自信可以讓你更好的完成面試。
Python 提供了自動(dòng)化的內(nèi)存管理,也就是說(shuō)內(nèi)存空間的分配與釋放都是由 Python 解釋器在運(yùn)行時(shí)自動(dòng)進(jìn)行的,自動(dòng)管理內(nèi)存功能極大的減輕程序員的工作負(fù)擔(dān),也能夠幫助程序員在一定程度上解決內(nèi)存泄露的問(wèn)題。
以 CPython 解釋器為例,它的內(nèi)存管理有三個(gè)關(guān)鍵點(diǎn):引用計(jì)數(shù)、標(biāo)記清理、分代收集。
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
引用計(jì)數(shù):對(duì)于 CPython 解釋器來(lái)說(shuō),Python 中的每一個(gè)對(duì)象其實(shí)就是 PyObject 結(jié)構(gòu)體,它的內(nèi)部有一個(gè)名為 ob_refcnt 的引用計(jì)數(shù)器成員變量。程序在運(yùn)行的過(guò)程中 ob_refcnt 的值會(huì)被更新,并用 ob_refcnt 來(lái)反映有多少個(gè)變量引用到該對(duì)象。當(dāng)對(duì)象的引用計(jì)數(shù)值為 0 時(shí),它的內(nèi)存就會(huì)被釋放掉。
以下情況會(huì)導(dǎo)致引用計(jì)數(shù)加 1:
對(duì)象被創(chuàng)建 對(duì)象被引用 對(duì)象作為參數(shù)傳入到一個(gè)函數(shù)中 對(duì)象作為元素存儲(chǔ)到一個(gè)容器中
以下情況會(huì)導(dǎo)致引用計(jì)數(shù)減 1:
用 del語(yǔ)句顯示刪除對(duì)象引用對(duì)象引用被重新賦值其他對(duì)象 一個(gè)對(duì)象離開(kāi)它所在的作用域 持有該對(duì)象的容器自身被銷毀 持有該對(duì)象的容器刪除該對(duì)象
可以通過(guò) sys 模塊的 getrefcount 函數(shù)來(lái)獲得對(duì)象的引用計(jì)數(shù)。引用計(jì)數(shù)的內(nèi)存管理方式在遇到循環(huán)引用的時(shí)候就會(huì)出現(xiàn)致命傷,因此需要其他的垃圾回收算法對(duì)其進(jìn)行補(bǔ)充。
標(biāo)記清理
CPython使用了“標(biāo)記-清理”(Mark and Sweep)算法解決容器類型可能產(chǎn)生的循環(huán)引用問(wèn)題。該算法在垃圾回收時(shí)分為兩個(gè)階段:標(biāo)記階段,遍歷所有的對(duì)象,如果對(duì)象是可達(dá)的(被其他對(duì)象引用),那么就標(biāo)記該對(duì)象為可達(dá);清除階段,再次遍歷對(duì)象,如果發(fā)現(xiàn)某個(gè)對(duì)象沒(méi)有標(biāo)記為可達(dá),則就將其回收。
CPython 底層維護(hù)了兩個(gè)雙端鏈表,一個(gè)鏈表存放著需要被掃描的容器對(duì)象,姑且稱之為鏈表 A,另一個(gè)鏈表存放著臨時(shí)不可達(dá)對(duì)象,姑且稱之為鏈表 B。為了實(shí)現(xiàn)“標(biāo)記-清理”算法,鏈表中的每個(gè)節(jié)點(diǎn)除了有記錄當(dāng)前引用計(jì)數(shù)的 ref_count 變量外,還有一個(gè) gc_ref 變量,這個(gè) gc_ref 是 ref_count 的一個(gè)副本,所以初始值為 ref_count 的大小。執(zhí)行垃圾回收時(shí),首先遍歷鏈表 A 中的節(jié)點(diǎn),并且將當(dāng)前對(duì)象所引用的所有對(duì)象的 gc_ref 減 1,這一步主要作用是解除循環(huán)引用對(duì)引用計(jì)數(shù)的影響。再次遍歷鏈表 A 中的節(jié)點(diǎn),如果節(jié)點(diǎn)的gc_ref值為0,那么這個(gè)對(duì)象就被標(biāo)記為“暫時(shí)不可達(dá)”(GC_TENTATIVELY_UNREACHABLE)并被移動(dòng)到鏈表B中;如果節(jié)點(diǎn)的gc_ref不為0,那么這個(gè)對(duì)象就會(huì)被標(biāo)記為“可達(dá)”(GC_REACHABLE),對(duì)于“可達(dá)”對(duì)象,還要遞歸的將該節(jié)點(diǎn)可以到達(dá)的節(jié)點(diǎn)標(biāo)記為“可達(dá)”;鏈表B中被標(biāo)記為“可達(dá)”的節(jié)點(diǎn)要重新放回到鏈表A中。在兩次遍歷之后,鏈表 B 中的節(jié)點(diǎn)就是需要釋放內(nèi)存的節(jié)點(diǎn)。
分代回收
在循環(huán)引用對(duì)象的回收中,整個(gè)應(yīng)用程序會(huì)被暫停,為了減少應(yīng)用程序暫停的時(shí)間,Python 通過(guò)分代回收(空間換時(shí)間)的方法提高垃圾回收效率。分代回收的基本思想是:對(duì)象存在的時(shí)間越長(zhǎng),是垃圾的可能性就越小,應(yīng)該盡量不對(duì)這樣的對(duì)象進(jìn)行垃圾回收。CPython將對(duì)象分為三種世代分別記為 0、1、2,每一個(gè)新生對(duì)象都在第 0 代中,如果該對(duì)象在一輪垃圾回收掃描中存活下來(lái),那么它將被移到第 1 代中,存在于第 1 代的對(duì)象將較少的被垃圾回收掃描到;如果在對(duì)第 1 代進(jìn)行垃圾回收掃描時(shí),這個(gè)對(duì)象又存活下來(lái),那么它將被移至第 2 代中,在那里它被垃圾回收掃描的次數(shù)將會(huì)更少。分代回收掃描的門限值可以通過(guò) gc 模塊的 get_threshold 函數(shù)來(lái)獲得,該函數(shù)返回一個(gè)三元組,分別表示多少次內(nèi)存分配操作后會(huì)執(zhí)行 0 代垃圾回收,多少次 0 代垃圾回收后會(huì)執(zhí)行 1 代垃圾回收,多少次 1 代垃圾回收后會(huì)執(zhí)行 2 代垃圾回收。需要說(shuō)明的是,如果執(zhí)行一次 2 代垃圾回收,那么比它年輕的代都要執(zhí)行垃圾回收。如果想修改這幾個(gè)門限值,可以通過(guò) gc 模塊的 set_threshold 函數(shù)來(lái)做到。
最后的話
學(xué)習(xí)一門編程語(yǔ)言,一定要弄明白它是如何管理內(nèi)存的,這不僅是如何應(yīng)付面試的問(wèn)題,更是如何更好的使用編程語(yǔ)言的基礎(chǔ)。內(nèi)存管理的一些算法設(shè)計(jì),也有助于我們應(yīng)對(duì)一些復(fù)雜的系統(tǒng)設(shè)計(jì),學(xué)好它很有必要。
學(xué)習(xí)無(wú)止境,學(xué)的越多,就越覺(jué)得不知道的越多,但是學(xué)的越多,就越知道自己的邊界,也就越不怕未知,這也是學(xué)習(xí)的意義。
參考資料
原文: https://github.com/jackfrued/Python-Interview-Bible/blob/master/Python面試寶典-基礎(chǔ)篇-2020.md
