首先要記住一句話:
“Python采用的是引用計(jì)數(shù)機(jī)制為主,標(biāo)記清除和分代收集兩種機(jī)制為輔的策略疼阔。”
引用計(jì)數(shù)器
首先來(lái)了解一下引用計(jì)數(shù)器半夷,因?yàn)槭?strong>以引用計(jì)數(shù)器為主的機(jī)制婆廊。
1.環(huán)狀雙向鏈表
在Python中,有一個(gè)環(huán)狀的雙向鏈表叫refchain巫橄,任何對(duì)象都會(huì)被放在這個(gè)環(huán)狀雙向鏈表中淘邻。
name = "cat"
age = 10
hobby = ["eat fish", "sleep"]
上面的代碼一執(zhí)行,會(huì)創(chuàng)建三個(gè)對(duì)象湘换,一個(gè)字符串對(duì)象宾舅,一個(gè)整型對(duì)象,一個(gè)列表對(duì)象彩倚,這三個(gè)對(duì)象都會(huì)被放到這個(gè)鏈表中筹我。
當(dāng)python代碼遇到name = "cat"
,內(nèi)部會(huì)創(chuàng)建一些數(shù)據(jù)(C語(yǔ)言源碼是創(chuàng)建了一個(gè)結(jié)構(gòu)體):上一個(gè)對(duì)象帆离,下一個(gè)對(duì)象蔬蕊,類(lèi)型,引用的個(gè)數(shù)等等哥谷,當(dāng)前對(duì)象的類(lèi)型是字符串岸夯,引用的個(gè)數(shù)是1,因?yàn)閚ame這個(gè)變量名引用了當(dāng)前這個(gè)對(duì)象们妥,如果new = name
猜扮,那么這個(gè)引用計(jì)數(shù)會(huì)加一。
如果這個(gè)對(duì)象是由多個(gè)元素組成的监婶,還會(huì)有一個(gè)值記錄它的元素個(gè)數(shù)旅赢。
C的源碼:
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
// 宏定義,包含 上一個(gè)压储、下一個(gè)鲜漩,用于構(gòu)造雙向鏈表用源譬。(放到refchain鏈表中時(shí)集惋,要用到)
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA // 用于構(gòu)造雙向鏈表
Py_ssize_t ob_refcnt; // 引用計(jì)數(shù)器
struct _typeobject *ob_type; // 數(shù)據(jù)類(lèi)型
} PyObject;
typedef struct {
PyObject ob_base; // PyObject對(duì)象
Py_ssize_t ob_size; /* Number of items in variable part,即:元素個(gè)數(shù) */
} PyVarObject;
如果是不同類(lèi)型的數(shù)據(jù)踩娘,內(nèi)部會(huì)創(chuàng)建以下的內(nèi)容:
- float類(lèi)型
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
- int類(lèi)型
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
/* Long (arbitrary precision) integer object interface */
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
- str類(lèi)型
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* Number of code points in the string */
Py_hash_t hash; /* Hash value; -1 if not set */
struct {
unsigned int interned:2;
/* Character size:
- PyUnicode_WCHAR_KIND (0):
* character type = wchar_t (16 or 32 bits, depending on the
platform)
- PyUnicode_1BYTE_KIND (1):
* character type = Py_UCS1 (8 bits, unsigned)
* all characters are in the range U+0000-U+00FF (latin1)
* if ascii is set, all characters are in the range U+0000-U+007F
(ASCII), otherwise at least one character is in the range
U+0080-U+00FF
- PyUnicode_2BYTE_KIND (2):
* character type = Py_UCS2 (16 bits, unsigned)
* all characters are in the range U+0000-U+FFFF (BMP)
* at least one character is in the range U+0100-U+FFFF
- PyUnicode_4BYTE_KIND (4):
* character type = Py_UCS4 (32 bits, unsigned)
* all characters are in the range U+0000-U+10FFFF
* at least one character is in the range U+10000-U+10FFFF
*/
unsigned int kind:3;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
unsigned int :24;
} state;
wchar_t *wstr; /* wchar_t representation (null-terminated) */
} PyASCIIObject;
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
* terminating \0. */
char *utf8; /* UTF-8 representation (null-terminated) */
Py_ssize_t wstr_length; /* Number of code points in wstr, possible
* surrogates count as two code points. */
} PyCompactUnicodeObject;
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;
- list類(lèi)型
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
- tuple類(lèi)型
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
- dict類(lèi)型
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
其中:
- _ob_next : refchian中的上一個(gè)對(duì)象
- _ob_prev:refchain中的下一個(gè)對(duì)象
- ob_refnt:引用計(jì)數(shù)器
- ob_type:是當(dāng)前對(duì)象的類(lèi)型
- ob_fval:是這個(gè)對(duì)象的值
當(dāng)python運(yùn)行程序時(shí)刮刑,會(huì)根據(jù)數(shù)據(jù)類(lèi)型的不同找到它對(duì)應(yīng)的結(jié)構(gòu)體喉祭,根據(jù)結(jié)構(gòu)體中的字段來(lái)進(jìn)行創(chuàng)建相關(guān)數(shù)據(jù),然后將這個(gè)對(duì)象添加到refchain中雷绢。
大體機(jī)制:
每個(gè)對(duì)象中有ob_refcnt就是引用計(jì)數(shù)器泛烙,默認(rèn)值為1,當(dāng)有其他變量引用對(duì)象時(shí)翘紊,引用計(jì)數(shù)器的值會(huì)增加蔽氨,如果引用這個(gè)對(duì)象的變量被刪除或者引用別的對(duì)象了,那么這個(gè)引用計(jì)數(shù)器的值會(huì)減小帆疟,當(dāng)引用計(jì)數(shù)器的值變?yōu)?時(shí)鹉究,意味著沒(méi)有變量在使用這個(gè)對(duì)象了,那么這個(gè)對(duì)象就變成了需要被刪除的垃圾踪宠,系統(tǒng)就會(huì)將這個(gè)對(duì)象從refchian里面移除自赔,將對(duì)象銷(xiāo)毀,把這塊內(nèi)存還給系統(tǒng)柳琢。
2.單純使用引用計(jì)數(shù)器進(jìn)行垃圾回收的問(wèn)題:循環(huán)引用
如果是這樣的一個(gè)代碼:
# refchian中創(chuàng)建一個(gè)列表對(duì)象绍妨,因?yàn)関1=對(duì)象,所以對(duì)象的引用計(jì)數(shù)器為1
v1 = [11,22,33]
# 同理為1
v2 = [44,55,66]
# 把v2追加到v1中柬脸,那么[11,22,33]的引用計(jì)數(shù)器就會(huì)加一他去,變?yōu)?
v1.append(v2)
# 同理為2
v2.append(v1)
這時(shí)候,如果刪除了變量v1和變量v2:
# [11,22,33] 的引用計(jì)數(shù)器減一倒堕,變?yōu)?
del v1
# [44,55,66]同理為1
del v2
這時(shí)候就會(huì)有一個(gè)問(wèn)題孤页,就是已經(jīng)沒(méi)有變量引用對(duì)象[11,22,33]和[44,55,66]了,但是因?yàn)樗麄兊囊糜?jì)數(shù)器不為0涩馆,這兩個(gè)對(duì)象的內(nèi)存空間沒(méi)有被回收行施,所以他們會(huì)永遠(yuǎn)在內(nèi)存中,而又永遠(yuǎn)不會(huì)被使用到魂那。
所以為了解決這個(gè)問(wèn)題蛾号,就有了標(biāo)記清除。
標(biāo)記清除
標(biāo)記清除就是為了解決引用計(jì)數(shù)器循環(huán)引用的問(wèn)題涯雅。
實(shí)習(xí)方法就是在Python的底層再去維護(hù)一個(gè)環(huán)形雙向鏈表鲜结,這個(gè)鏈表用來(lái)存放可能存在循環(huán)引用問(wèn)題的對(duì)象。(只有對(duì)象可以再放其他元素的對(duì)象才會(huì)出現(xiàn)循環(huán)引用問(wèn)題活逆,列表精刷,字典,元組和集合)
在Python內(nèi)部蔗候,在某種情況下怒允,回去掃描這個(gè)存放可能存在循環(huán)引用問(wèn)題的對(duì)象的鏈表,如果發(fā)現(xiàn)有循環(huán)引用锈遥,就把雙方的引用計(jì)數(shù)器都減一纫事,如果引用計(jì)數(shù)器減為0勘畔,回收內(nèi)存。
但是標(biāo)記清除也存在問(wèn)題:
- 什么時(shí)候會(huì)掃描一次
- 掃描一次存在耗時(shí)久的問(wèn)題
所以又引入了分代回收丽惶。
分代回收
實(shí)現(xiàn)方式就是把標(biāo)記清除的那個(gè)鏈表分成了三個(gè)鏈表炫七,這三個(gè)鏈表分別是:0代,1代钾唬,2代万哪。
- 當(dāng)0代中的對(duì)象個(gè)數(shù)超過(guò)700個(gè),掃描一次0代抡秆。
- 0代如果掃描10次壤圃,則1代掃描一次。
- 1代掃描10次琅轧,2代掃描一次伍绳。
三個(gè)鏈表的閾值是不同的,0代是對(duì)象個(gè)數(shù)乍桂,1代和2代都是前一代的掃描次數(shù)冲杀。
總結(jié)
在Python中維護(hù)了一個(gè)叫refchain的雙向環(huán)狀鏈表,這個(gè)鏈表用來(lái)存儲(chǔ)程序創(chuàng)建的所有對(duì)象睹酌,每種類(lèi)型的對(duì)象都有一個(gè)ob_refcnt引用計(jì)數(shù)器的值权谁,當(dāng)有變量引用對(duì)象時(shí),引用計(jì)數(shù)器的值就會(huì)加一憋沿,當(dāng)引用對(duì)象的變量減少了的時(shí)候旺芽,引用計(jì)數(shù)器的值就會(huì)減一,當(dāng)引用計(jì)數(shù)器變?yōu)?時(shí)辐啄,就會(huì)進(jìn)行垃圾回收采章。
但是在Python中,對(duì)于有多個(gè)元素組成的對(duì)象壶辜,可能還會(huì)有循環(huán)引用的問(wèn)題悯舟,為了解決這個(gè)問(wèn)題,Python又引用了標(biāo)記清除和分代回收砸民,所以在Python內(nèi)部實(shí)際上要維護(hù)四個(gè)雙向環(huán)狀鏈表抵怎,分別是:
- refchian
- 0代 700個(gè)
- 1代 0代掃描10次
- 2代 1代掃描10次
在源碼內(nèi)部,當(dāng)達(dá)到各自的閾值時(shí)岭参,就會(huì)掃描鏈表反惕,發(fā)現(xiàn)循環(huán)引用就會(huì)想相關(guān)對(duì)象的引用計(jì)數(shù)器減一,如果引用計(jì)數(shù)器的值被減為0演侯,那么這個(gè)對(duì)象就會(huì)被銷(xiāo)毀姿染,這個(gè)對(duì)象占用的內(nèi)存空間就會(huì)被回收。
優(yōu)化:緩存
在Python內(nèi)部蚌本,源碼對(duì)上述過(guò)程進(jìn)行了優(yōu)化盔粹,這個(gè)優(yōu)化就是緩存。
1.內(nèi)存池
為了避免重復(fù)創(chuàng)建和銷(xiāo)毀一些常見(jiàn)對(duì)象程癌,就會(huì)維護(hù)一個(gè)內(nèi)存池舷嗡。
在啟動(dòng)Python解釋器時(shí),內(nèi)部會(huì)自動(dòng)為我們創(chuàng)建-5~256這些數(shù)字放到內(nèi)存池中嵌莉,如果有變量需要指向這些值进萄,內(nèi)存不會(huì)開(kāi)辟新的內(nèi)存,直接從內(nèi)存池中取锐峭。
所以如果:
v1 = 8
v2 = 8
打印一下v1和v2的id中鼠,會(huì)發(fā)現(xiàn)他們的id是相同的。
而且內(nèi)存池中的對(duì)象沿癞,他們的引用計(jì)數(shù)器不會(huì)變?yōu)?援雇,因?yàn)槌跏蓟臅r(shí)候,他們的引用計(jì)數(shù)器就為1椎扬,這時(shí)候沒(méi)有變量引用他們惫搏,所以當(dāng)引用他們的變量引用后又不再引用他們的時(shí)候,他們的引用計(jì)數(shù)器也不會(huì)變?yōu)?蚕涤。
2.free_list
當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)器變?yōu)?時(shí)筐赔,先不回收,而是把這個(gè)對(duì)象放到free_list中揖铜,當(dāng)作緩存茴丰,這樣再創(chuàng)建對(duì)象時(shí),就不開(kāi)辟新的內(nèi)存天吓,直接使用free_list中的對(duì)象的內(nèi)存贿肩,把這塊內(nèi)存初始化,再把這個(gè)對(duì)象放到refchain中龄寞。
當(dāng)然這個(gè)緩沖池也是有大小限制的尸曼,如果一個(gè)對(duì)象的引用計(jì)數(shù)器變?yōu)?,而此時(shí)緩沖池也已經(jīng)滿了萄焦,那么這個(gè)對(duì)象還是會(huì)被直接銷(xiāo)毀的控轿。
float,list拂封,tuple茬射,dict采用這種機(jī)制。
- float類(lèi)型冒签,維護(hù)的free_list鏈表最多可緩存100個(gè)float對(duì)象在抛。
- list類(lèi)型,維護(hù)的free_list鏈表最多可緩存80個(gè)list對(duì)象萧恕。
- dict類(lèi)型刚梭,維護(hù)的free_list鏈表最多可緩存80個(gè)dict對(duì)象肠阱。
tuple類(lèi)型的比較特殊,可以理解為如果是tuple類(lèi)型的朴读,free_list的容量為20屹徘,這時(shí)候的free_list有20個(gè)index,index從0到19衅金,每個(gè)索引位置都存放了一個(gè)鏈表噪伊,index為0的位置,存放的是空元組氮唯,index為1的位置存放的是元組長(zhǎng)度為1的元組鉴吹,這樣以此類(lèi)推。每一個(gè)鏈表都可以存放2000個(gè)元組惩琉。
字符串類(lèi)型的有兩種優(yōu)化方式:
1.字符池
2.字符串駐留機(jī)制
- 字符池就是和int的內(nèi)存池類(lèi)似豆励,python內(nèi)部會(huì)將ASCII的所有字符存在一個(gè)叫
unicode_latin[256]
的鏈表中。 - 字符串駐留機(jī)制:python會(huì)將只有字母瞒渠、數(shù)字肆糕、下劃線并且長(zhǎng)度不大于20的字符串進(jìn)行駐留,放到內(nèi)存在孝,如果下次再創(chuàng)建一個(gè)一模一樣的值诚啃,就不再開(kāi)辟新的內(nèi)存空間。