一個(gè) Python 對(duì)象會(huì)在何時(shí)被銷毀?
楔子
如果對(duì)編程語言進(jìn)行分類的話,一般可以分為靜態(tài)語言和動(dòng)態(tài)語言,也可以分為編譯型語言和解釋型語言。但個(gè)人覺得還可以有一種劃分標(biāo)準(zhǔn),就是是否自帶垃圾回收。關(guān)于有沒有垃圾回收,陳儒老師在《Python 2.5源碼剖析》中,總結(jié)得非常好。
對(duì)于像 C 和 C++ 這類語言,程序員被賦予了極大的自由,可以任意地申請(qǐng)內(nèi)存。但權(quán)力的另一面對(duì)應(yīng)著責(zé)任,程序員最后不使用的時(shí)候,必須負(fù)責(zé)將申請(qǐng)的內(nèi)存釋放掉,并把無效指針設(shè)置為空??梢哉f,這一點(diǎn)是萬惡之源,大量內(nèi)存泄漏、懸空指針、越界訪問的 bug 由此產(chǎn)生。
而現(xiàn)代的開發(fā)語言(比如 C#、Java)都帶有垃圾回收機(jī)制,將開發(fā)人員從維護(hù)內(nèi)存分配和清理的繁重工作中解放出來,開發(fā)者不用再擔(dān)心內(nèi)存泄漏的問題,但同時(shí)也剝奪了程序員和內(nèi)存親密接觸的機(jī)會(huì),并犧牲了一定的運(yùn)行效率。不過好處就是提高了開發(fā)效率,并降低了 bug 發(fā)生的概率。
由于現(xiàn)在的垃圾回收機(jī)制已經(jīng)非常成熟了,把對(duì)性能的影響降到了最低,因此大部分場景選擇的都是帶垃圾回收的語言。
而 Python 里面同樣具有垃圾回收,只不過它是為引用計(jì)數(shù)機(jī)制服務(wù)的。所以解釋器通過內(nèi)部的引用計(jì)數(shù)和垃圾回收,代替程序員進(jìn)行繁重的內(nèi)存管理工作,關(guān)于垃圾回收我們后面會(huì)詳細(xì)說,先來看一下引用計(jì)數(shù)。
引用計(jì)數(shù)
Python 一切皆對(duì)象,所有對(duì)象都有一個(gè) ob_refcnt 字段,該字段維護(hù)著對(duì)象的引用計(jì)數(shù),從而也決定對(duì)象的存在與消亡。下面來探討一下引用計(jì)數(shù),當(dāng)然引用計(jì)數(shù)在介紹 PyObject 的時(shí)候說的很詳細(xì)了,這里再回顧一下。
但需要說明的是,比起類型對(duì)象,我們更關(guān)注實(shí)例對(duì)象的行為。引用計(jì)數(shù)也是如此,只有實(shí)例對(duì)象,我們探討引用計(jì)數(shù)才是有意義的。
因?yàn)閮?nèi)置的類型對(duì)象超越了引用計(jì)數(shù)規(guī)則,永遠(yuǎn)都不會(huì)被析構(gòu),或者銷毀,因?yàn)樗鼈冊(cè)诘讓邮潜混o態(tài)定義好的。
圖片
很明顯,內(nèi)置的類型對(duì)象屬于永恒對(duì)象。關(guān)于永恒對(duì)象之前解釋過,指的是那些永遠(yuǎn)不會(huì)被回收的對(duì)象,像 None、小整數(shù)對(duì)象池里面的整數(shù)、以及內(nèi)置的類型對(duì)象,它們都是永恒對(duì)象。
如果對(duì)象是永恒對(duì)象,那么它的引用計(jì)數(shù)會(huì)直接被初始化為 uint32 最大值。當(dāng)然,如果一個(gè)對(duì)象原本不是永恒對(duì)象,但它的引用計(jì)數(shù)之后達(dá)到了 uint32 最大值(有 2 ** 32 - 1 個(gè)變量在引用它),那么它也會(huì)被判定為永恒對(duì)象,但很明顯這只是理論情況,現(xiàn)實(shí)不可能出現(xiàn),因?yàn)橐粋€(gè)對(duì)象不可能有這么多的變量在引用它。
同理,我們自定義的類,雖然可以被回收,但是探討它的引用計(jì)數(shù)也是沒有價(jià)值的。我們舉個(gè)栗子:
class A:
pass
del A
首先 del 關(guān)鍵字只能作用于變量,不可以作用于對(duì)象,比如 e = 2.71,可以 del e,但是不可以 del 2.71,這是不符合語法規(guī)則的。因?yàn)?del 的作用是刪除變量,并讓其指向?qū)ο蟮囊糜?jì)數(shù)減 1,所以我們只能 del 變量,不可以 del 對(duì)象。
同樣的,使用 def、class 關(guān)鍵字定義完之后拿到的也是變量,比如上面代碼中的 A,只要是變量,就可以被 del。但是 del 變量只是刪除了該變量,換言之就是讓該變量無法再被使用,至于變量指向的對(duì)象是否會(huì)被回收,就看是否還有其它的變量也指向它。
總結(jié):對(duì)象是否被回收完全由解釋器判斷它的引用計(jì)數(shù)是否為 0 所決定。
永恒對(duì)象
我們一直說對(duì)象的 ob_refcnt 字段負(fù)責(zé)維護(hù)引用計(jì)數(shù),當(dāng)然這是沒問題的。但 Python 從 3.12 開始又引入了 ob_refcnt_split 字段,也負(fù)責(zé)維護(hù)引用計(jì)數(shù)。
圖片
ob_refcnt_split 是一個(gè)長度為 2、類型為 uint32 的數(shù)組,但只會(huì)用其中一個(gè)元素來維護(hù)引用計(jì)數(shù)。如果達(dá)到了 uint32 最大值,那么判定為永恒對(duì)象,相關(guān)源碼后續(xù)聊。
我們來看看永恒對(duì)象的初始化過程,以 list 類型對(duì)象為例,看看它的引用計(jì)數(shù)是怎么設(shè)置的。
// Objects/listobject.c
// 引用計(jì)數(shù)和類型由宏 PyVarObject_HEAD_INIT 負(fù)責(zé)設(shè)置
PyTypeObject PyList_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list",
sizeof(PyListObject),
0,
...
};
// Include/object.h
#define PyVarObject_HEAD_INIT(type, size) \
{ \
PyObject_HEAD_INIT(type) \
(size) \
},
#define PyObject_HEAD_INIT(type) \
{ \
{ _Py_IMMORTAL_REFCNT }, \
(type) \
},
#define _Py_IMMORTAL_REFCNT UINT_MAX
我們看到類型對(duì)象在初始化的時(shí)候,引用計(jì)數(shù)直接被設(shè)置成了 uint32 最大值。當(dāng)然啦,這并不是說有 2 ** 32 - 1 個(gè)變量在引用,而是通過將引用計(jì)數(shù)設(shè)置為 uint32 最大值,來表示這是一個(gè)不會(huì)被銷毀的永恒對(duì)象。
源碼解密引用計(jì)數(shù)的相關(guān)操作
操作引用計(jì)數(shù)無非就是將其加一或減一,至于什么時(shí)候加一、什么時(shí)候減一,在介紹 PyObject 的時(shí)候已經(jīng)說的很詳細(xì)了,可以看一下。這里我們通過源碼,看看引用計(jì)數(shù)具體是怎么操作的。
在底層,解釋器會(huì)通過 Py_INCREF 和 Py_DECREF 兩個(gè)函數(shù)來增加和減少對(duì)象的引用計(jì)數(shù),而當(dāng)對(duì)象的引用計(jì)數(shù)減少到 0 后,Py_DECREF 將調(diào)用對(duì)應(yīng)的析構(gòu)函數(shù)來釋放該對(duì)象所占的內(nèi)存和系統(tǒng)資源。這個(gè)析構(gòu)函數(shù)由對(duì)象的類型對(duì)象中定義的函數(shù)指針來指定,也就是 tp_dealloc。
下面我們來看看底層實(shí)現(xiàn),不過在介紹 Py_INCREF 和 Py_DECREF 之前,先來看幾個(gè)其它的函數(shù),這些函數(shù)非常常見,有必要單獨(dú)說一下。
// Include/object.h
// 返回對(duì)象的引用計(jì)數(shù),說白了就是獲取對(duì)象的 ob_refcnt 字段
// 因?yàn)樵撟侄呜?fù)責(zé)維護(hù)引用計(jì)數(shù)
static inline Py_ssize_t Py_REFCNT(PyObject *ob) {
return ob->ob_refcnt;
}
// 設(shè)置對(duì)象的引用計(jì)數(shù)
static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
// 如果對(duì)象是永恒對(duì)象,那么直接返回
// 不會(huì)再對(duì)永恒對(duì)象的引用計(jì)數(shù)做任何設(shè)置
if (_Py_IsImmortal(ob)) {
return;
}
ob->ob_refcnt = refcnt;
}
// 返回對(duì)象的類型,獲取 ob_type 字段
static inline PyTypeObject* Py_TYPE(PyObject *ob) {
return ob->ob_type;
}
// 設(shè)置對(duì)象的類型
static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) {
ob->ob_type = type;
}
// 返回對(duì)象的 ob_size
static inline Py_ssize_t Py_SIZE(PyObject *ob) {
// _PyVarObject_CAST(ob) 等價(jià)于 (PyVarObject *)(ob)
return _PyVarObject_CAST(ob)->ob_size;
}
// 設(shè)置對(duì)象的 ob_size
static inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) {
ob->ob_size = size;
}
這幾個(gè)函數(shù)是用來設(shè)置引用計(jì)數(shù)、類型和 ob_size 的,比較簡單,即使不看源碼也能猜出內(nèi)部都做了什么。需要注意的是,這些函數(shù)在之前的 Python 源碼中都是以宏的形式存在,但在 3.12 里面變成內(nèi)聯(lián)函數(shù)了,本質(zhì)上沒有太大差異。
然后來看看 Py_INCREF 和 Py_DECREF,它們負(fù)責(zé)對(duì)引用計(jì)數(shù)執(zhí)行加一和減一操作。
注意:這兩個(gè)函數(shù)里面存在宏判斷,我們這里只保留判斷之后的結(jié)果。
// Include/object.h
static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op)
{
// ob_refcnt_split 是長度為 2 的數(shù)組,但只會(huì)使用一個(gè)元素
// 至于使用哪一個(gè),則取決于字節(jié)序,是大端存儲(chǔ)還是小端存儲(chǔ)
PY_UINT32_T cur_refcnt = op->ob_refcnt_split[PY_BIG_ENDIAN];
// 將當(dāng)前引用計(jì)數(shù)加一
PY_UINT32_T new_refcnt = cur_refcnt + 1;
// 如果 cur_refcnt 已經(jīng)達(dá)到了 uint32 最大值,那么加一之后會(huì)產(chǎn)生環(huán)繞,繼續(xù)從零開始
// 所以如果 new_refcnt 為 0,證明當(dāng)前對(duì)象的引用計(jì)數(shù)為 uint32 最大值
// 那么該對(duì)象就是永恒對(duì)象,而永恒對(duì)象不會(huì)被回收,引用計(jì)數(shù)也不再做處理,因此直接返回
if (new_refcnt == 0) {
return;
}
// 否則說明不是引用計(jì)數(shù),那么進(jìn)行更新
op->ob_refcnt_split[PY_BIG_ENDIAN] = new_refcnt;
// 稍后解釋
_Py_INCREF_STAT_INC();
}
這里估計(jì)有人發(fā)現(xiàn)了一個(gè)問題,就是當(dāng)前只更新了 ob_refcnt_split,而沒有更新 ob_refcnt。原因很簡單,因?yàn)檫@兩個(gè)字段組成的是共同體,它們占用同一份內(nèi)存。
ob_refcnt 是 int64 整數(shù),ob_refcnt_split 是長度為 2 的 uint32 數(shù)組,它們都是 8 字節(jié),并且占用的是同一份 8 字節(jié)的內(nèi)存。所以 ob_refcnt_split 里面的兩個(gè)元素正好對(duì)應(yīng) ob_refcnt 的低 32 位和高 32 位。
因此在修改 ob_refcnt_split 的時(shí)候,同時(shí)也修改了 ob_refcnt,所以整個(gè)操作只進(jìn)行了一次。并且從源碼中也可以看出,對(duì)象的引用計(jì)數(shù)不會(huì)超過 uint32 最大值,因?yàn)楫?dāng)達(dá)到這個(gè)值的時(shí)候會(huì)被判定為永恒對(duì)象,而永恒對(duì)象的引用計(jì)數(shù)不會(huì)再做任何操作,因?yàn)橛篮銓?duì)象會(huì)永遠(yuǎn)存在。
但還是那句話,除非一開始就將引用計(jì)數(shù)設(shè)置為 uint32 最大值,讓對(duì)象成為永恒對(duì)象,否則單靠創(chuàng)建變量是不可能讓對(duì)象的引用計(jì)數(shù)達(dá)到這一限制的,因?yàn)椴还茉購?fù)雜的項(xiàng)目,也不會(huì)出現(xiàn)一個(gè)對(duì)象被 2 ** 32 - 1 個(gè)變量指向的情況,所以 uint32 是完全夠用的。
然后在函數(shù)的最后出現(xiàn)了一個(gè) _Py_INCREF_STAT_INC 函數(shù),它負(fù)責(zé)對(duì)一些全局統(tǒng)計(jì)信息進(jìn)行更新,目前無需關(guān)注。
以上是 Py_INCREF,負(fù)責(zé)將引用計(jì)數(shù)加一,再來看看 Py_DECREF,它負(fù)責(zé)將引用計(jì)數(shù)減一。
// Include/object.h
static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
// 如果對(duì)象是永恒對(duì)象,那么直接返回,因?yàn)橛篮銓?duì)象不會(huì)被回收
// 它的引用計(jì)數(shù)不會(huì)再發(fā)生變化,始終保持 uint32 最大值
if (_Py_IsImmortal(op)) {
return;
}
// 更新一些全局統(tǒng)計(jì)信息,和 _Py_INCREF_STAT_INC 作用一樣
_Py_DECREF_STAT_INC();
// 重點(diǎn)來了,首先將 ob_refcnt 減一,然后判斷它是否等于 0
// 如果為 0,說明對(duì)象已經(jīng)不被任何變量引用了,那么應(yīng)該被銷毀
if (--op->ob_refcnt == 0) {
// 調(diào)用 _Py_Dealloc 將對(duì)象銷毀,這個(gè)函數(shù)內(nèi)部的邏輯很簡單
// 雖然里面存在很多宏判斷,導(dǎo)致代碼看起來很復(fù)雜
// 但如果只看編譯后的最終結(jié)果,那么代碼就只有下面三行
/*
PyTypeObject *type = Py_TYPE(op);
destructor dealloc = type->tp_dealloc;
(*dealloc)(op);
*/
// 會(huì)獲取類型對(duì)象的 tp_dealloc,然后調(diào)用,銷毀實(shí)例對(duì)象
_Py_Dealloc(op);
}
}
以上就是 Py_INCREF 和 Py_DECREF 兩個(gè)函數(shù)的具體實(shí)現(xiàn),但是它們不能接收空指針,如果希望能接收空指針,那么可以使用另外兩個(gè)函數(shù)。
圖片
Py_XINCREF 和 Py_XDECREF 會(huì)額外對(duì)指針做一次判斷,如果為空則什么也不做,不為空再調(diào)用 Py_INCREF 和 Py_DECREF。
在一個(gè)對(duì)象的引用計(jì)數(shù)為 0 時(shí),與該對(duì)象對(duì)應(yīng)的析構(gòu)函數(shù)就會(huì)被調(diào)用。但是要特別注意的是,我們之前說調(diào)用析構(gòu)函數(shù)之后會(huì)回收對(duì)象,或者銷毀對(duì)象、刪除對(duì)象等等,意思是將這個(gè)對(duì)象從內(nèi)存中抹去,但并不意味著要釋放空間。換句話說就是對(duì)象沒了,但對(duì)象占用的內(nèi)存卻有可能還在。
如果對(duì)象沒了,占用的內(nèi)存也要釋放的話,那么頻繁申請(qǐng)、釋放內(nèi)存空間會(huì)使 Python 的執(zhí)行效率大打折扣,更何況 Python 已經(jīng)背負(fù)了人們對(duì)其執(zhí)行效率的不滿這么多年。
所以 Python 底層大量采用了緩存池的技術(shù),使用這種技術(shù)可以避免頻繁地申請(qǐng)和釋放內(nèi)存空間。因此在析構(gòu)的時(shí)候,只是將對(duì)象占用的空間歸還到緩存池中,并沒有真的釋放。
這一點(diǎn),在后面剖析內(nèi)置實(shí)例對(duì)象的實(shí)現(xiàn)中,將會(huì)看得一清二楚,因?yàn)榇蟛糠謨?nèi)置的實(shí)例對(duì)象都會(huì)有自己的緩存池。
小結(jié)
到此我們的基礎(chǔ)概念就算說完了,從下一篇文章開始就要詳細(xì)剖析內(nèi)置對(duì)象的底層實(shí)現(xiàn)了,比如浮點(diǎn)數(shù)、復(fù)數(shù)、整數(shù)、布爾值、None、bytes 對(duì)象、bytearray 對(duì)象、字符串、元組、列表、字典、集合等等,所有的內(nèi)置對(duì)象都會(huì)詳細(xì)地剖析一遍,看看它是如何實(shí)現(xiàn)的。
有了目前為止的這些基礎(chǔ),我們后面就會(huì)輕松很多,先把對(duì)象、變量等概念梳理清楚,然后再來搞這些數(shù)據(jù)結(jié)構(gòu)的底層實(shí)現(xiàn)。