零基礎(chǔ)小白Python學(xué)習(xí):垃圾回收機(jī)制剖析

1、垃圾回收

引用計數(shù)器為主、分代碼回收和標(biāo)記清除為輔

1.1 大管家refchain

在Python的C源碼中有一個名為refchain的環(huán)狀雙向鏈表,這個鏈表比較牛逼了宣决,因?yàn)镻ython程序中一旦創(chuàng)建對象都會把這個對象添加到refchain這個鏈表中。也就是說他保存著所有的對象昏苏。

1.2 引用計數(shù)器

在refchain中的所有對象內(nèi)部都有一個ob_refcnt用來保存當(dāng)前對象的引用計數(shù)器疲扎,顧名思義就是自己被引用的次數(shù)。

當(dāng)值被多次引用時候捷雕,不會在內(nèi)存中重復(fù)創(chuàng)建數(shù)據(jù),而是引用計數(shù)器+1 壹甥。 當(dāng)對象被銷毀時候同時會讓引用計數(shù)器-1,如果引用計數(shù)器為0救巷,則將對象從refchain鏈表中摘除,同時在內(nèi)存中進(jìn)行銷毀(暫不考慮緩存等特殊情況)句柠。

?

如果大家在學(xué)習(xí)中遇到困難浦译,想找一個python學(xué)習(xí)交流環(huán)境棒假,可以加入我們的python圈,裙號930900780精盅,可領(lǐng)取python學(xué)習(xí)資料帽哑,會節(jié)約很多時間,減少很多遇到的難題叹俏。

age = 18

number = age? # 對象18的引用計數(shù)器 + 1

del age? ? ? ? ? # 對象18的引用計數(shù)器 - 1

def run(arg):

? ? print(arg)

run(number)? # 剛開始執(zhí)行函數(shù)時妻枕,對象18引用計數(shù)器 + 1,當(dāng)函數(shù)執(zhí)行完畢之后粘驰,對象18引用計數(shù)器 - 1 屡谐。

num_list = [11,22,number] # 對象18的引用計數(shù)器 + 1

復(fù)制代碼

1.3 標(biāo)記清除&分代回收

基于引用計數(shù)器進(jìn)行垃圾回收非常方便和簡單,但他還是存在循環(huán)引用的問題蝌数,導(dǎo)致無法正常的回收一些數(shù)據(jù)愕掏,例如:

v1 = [11,22,33]? ? ? ? # refchain中創(chuàng)建一個列表對象,由于v1=對象顶伞,所以列表引對象用計數(shù)器為1.

v2 = [44,55,66]? ? ? ? # refchain中再創(chuàng)建一個列表對象饵撑,因v2=對象,所以列表對象引用計數(shù)器為1.

v1.append(v2)? ? ? ? # 把v2追加到v1中唆貌,則v2對應(yīng)的[44,55,66]對象的引用計數(shù)器加1滑潘,最終為2.

v2.append(v1)? ? ? ? # 把v1追加到v1中,則v1對應(yīng)的[11,22,33]對象的引用計數(shù)器加1挠锥,最終為2.

del v1? ? # 引用計數(shù)器-1

del v2? ? # 引用計數(shù)器-1

復(fù)制代碼

對于上述代碼會發(fā)現(xiàn)众羡,執(zhí)行del操作之后,沒有變量再會去使用那兩個列表對象蓖租,但由于循環(huán)引用的問題粱侣,他們的引用計數(shù)器不為0,所以他們的狀態(tài):永遠(yuǎn)不會被使用蓖宦、也不會被銷毀齐婴。項(xiàng)目中如果這種代碼太多,就會導(dǎo)致內(nèi)存一直被消耗稠茂,直到內(nèi)存被耗盡柠偶,程序崩潰。

為了解決循環(huán)引用的問題睬关,引入了標(biāo)記清除技術(shù)诱担,專門針對那些可能存在循環(huán)引用的對象進(jìn)行特殊處理,可能存在循環(huán)應(yīng)用的類型有:列表电爹、元組蔫仙、字典、集合丐箩、自定義類等那些能進(jìn)行數(shù)據(jù)嵌套的類型摇邦。

標(biāo)記清除:創(chuàng)建特殊鏈表專門用于保存 列表恤煞、元組、字典施籍、集合居扒、自定義類等對象,之后再去檢查這個鏈表中的對象是否存在循環(huán)引用丑慎,如果存在則讓雙方的引用計數(shù)器均 - 1 喜喂。

分代回收:對標(biāo)記清除中的鏈表進(jìn)行優(yōu)化,將那些可能存在循引用的對象拆分到3個鏈表立哑,鏈表稱為:0/1/2三代夜惭,每代都可以存儲對象和閾值,當(dāng)達(dá)到閾值時铛绰,就會對相應(yīng)的鏈表中的每個對象做一次掃描诈茧,除循環(huán)引用各自減1并且銷毀引用計數(shù)器為0的對象。

// 分代的C源碼

#define NUM_GENERATIONS 3

struct gc_generation generations[NUM_GENERATIONS] = {

? ? /* PyGC_Head,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? threshold,? ? count */

? ? {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)},? 700,? ? ? ? 0}, // 0代

? ? {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)},? 10,? ? ? ? 0}, // 1代

? ? {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)},? 10,? ? ? ? 0}, // 2代

};

復(fù)制代碼

特別注意:0代和1捂掰、2代的threshold和count表示的意義不同敢会。

0代,count表示0代鏈表中對象的數(shù)量这嚣,threshold表示0代鏈表對象個數(shù)閾值鸥昏,超過則執(zhí)行一次0代掃描檢查。 1代姐帚,count表示0代鏈表掃描的次數(shù)吏垮,threshold表示0代鏈表掃描的次數(shù)閾值,超過則執(zhí)行一次1代掃描檢查罐旗。 2代膳汪,count表示1代鏈表掃描的次數(shù),threshold表示1代鏈表掃描的次數(shù)閾值九秀,超過則執(zhí)行一2代掃描檢查遗嗽。

1.4 情景模擬

根據(jù)C語言底層并結(jié)合圖來講解內(nèi)存管理和垃圾回收的詳細(xì)過程。

第一步:當(dāng)創(chuàng)建對象age=19時鼓蜒,會將對象添加到refchain鏈表中痹换。

?

第二步:當(dāng)創(chuàng)建對象num_list = [11,22]時,會將列表對象添加到 refchain 和 generations 0代中都弹。

?

第三步:新創(chuàng)建對象使generations的0代鏈表上的對象數(shù)量大于閾值700時娇豫,要對鏈表上的對象進(jìn)行掃描檢查。

當(dāng)0代大于閾值后畅厢,底層不是直接掃描0代锤躁,而是先判斷2、1是否也超過了閾值。

如果2系羞、1代未達(dá)到閾值,則掃描0代霸琴,并讓1代的 count + 1 椒振。

如果2代已達(dá)到閾值,則將2梧乘、1澎迎、0三個鏈表拼接起來進(jìn)行全掃描,并將2选调、1夹供、0代的count重置為0.

如果1代已達(dá)到閾值,則講1仁堪、0兩個鏈表拼接起來進(jìn)行掃描哮洽,并將所有1、0代的count重置為0.

對拼接起來的鏈表在進(jìn)行掃描時弦聂,主要就是剔除循環(huán)引用和銷毀垃圾鸟辅,詳細(xì)過程為:

掃描鏈表,把每個對象的引用計數(shù)器拷貝一份并保存到 gc_refs中莺葫,保護(hù)原引用計數(shù)器匪凉。

再次掃描鏈表中的每個對象,并檢查是否存在循環(huán)引用捺檬,如果存在則讓各自的gc_refs減 1 再层。

再次掃描鏈表,將 gc_refs 為 0 的對象移動到unreachable鏈表中堡纬;不為0的對象直接升級到下一代鏈表中聂受。

處理unreachable鏈表中的對象的 析構(gòu)函數(shù) 和 弱引用,不能被銷毀的對象升級到下一代鏈表隐轩,能銷毀的保留在此鏈表饺饭。析構(gòu)函數(shù),指的就是那些定義了__del__方法的對象职车,需要執(zhí)行之后再進(jìn)行銷毀處理瘫俊。

最后將 unreachable 中的每個對象銷毀并在refchain鏈表中移除(不考慮緩存機(jī)制)。

至此悴灵,垃圾回收的過程結(jié)束扛芽。

1.5 緩存機(jī)制

從上文大家可以了解到當(dāng)對象的引用計數(shù)器為0時,就會被銷毀并釋放內(nèi)存积瞒。而實(shí)際上他不是這么的簡單粗暴川尖,因?yàn)榉磸?fù)的創(chuàng)建和銷毀會使程序的執(zhí)行效率變低。Python中引入了“緩存機(jī)制”機(jī)制茫孔。

例如:引用計數(shù)器為0時叮喳,不會真正銷毀對象被芳,而是將他放到一個名為 free_list 的鏈表中,之后會再創(chuàng)建對象時不會在重新開辟內(nèi)存馍悟,而是在free_list中將之前的對象來并重置內(nèi)部的值來使用畔濒。

float類型,維護(hù)的free_list鏈表最多可緩存100個float對象锣咒。

? v1 = 3.14? ? # 開辟內(nèi)存來存儲float對象侵状,并將對象添加到refchain鏈表。

? print( id(v1) ) # 內(nèi)存地址:4436033488

? del v1? ? # 引用計數(shù)器-1毅整,如果為0則在rechain鏈表中移除趣兄,不銷毀對象,而是將對象添加到float的free_list.

? v2 = 9.999? ? # 優(yōu)先去free_list中獲取對象悼嫉,并重置為9.999艇潭,如果free_list為空才重新開辟內(nèi)存。

? print( id(v2) ) # 內(nèi)存地址:4436033488

? # 注意:引用計數(shù)器為0時承粤,會先判斷free_list中緩存?zhèn)€數(shù)是否滿了暴区,未滿則將對象緩存,已滿則直接將對象銷毀辛臊。

復(fù)制代碼

int類型仙粱,不是基于free_list,而是維護(hù)一個small_ints鏈表保存常見數(shù)據(jù)(小數(shù)據(jù)池)彻舰,小數(shù)據(jù)池范圍:-5 <= value < 257伐割。即:重復(fù)使用這個范圍的整數(shù)時,不會重新開辟內(nèi)存刃唤。

? v1 = 38? ? # 去小數(shù)據(jù)池small_ints中獲取38整數(shù)對象隔心,將對象添加到refchain并讓引用計數(shù)器+1。

? print( id(v1))? #內(nèi)存地址:4514343712

? v2 = 38 # 去小數(shù)據(jù)池small_ints中獲取38整數(shù)對象尚胞,將refchain中的對象的引用計數(shù)器+1硬霍。

? print( id(v2) ) #內(nèi)存地址:4514343712

? # 注意:在解釋器啟動時候-5~256就已經(jīng)被加入到small_ints鏈表中且引用計數(shù)器初始化為1,

? # 代碼中使用的值時直接去small_ints中拿來用并將引用計數(shù)器+1即可笼裳。另外唯卖,small_ints中的數(shù)據(jù)引用計數(shù)器永遠(yuǎn)不會為0

? # (初始化時就設(shè)置為1了),所以也不會被銷毀躬柬。

復(fù)制代碼

str類型拜轨,維護(hù)unicode_latin1[256]鏈表,內(nèi)部將所有的ascii字符緩存起來允青,以后使用時就不再反復(fù)創(chuàng)建橄碾。

? v1 = "A"

? print( id(v1) ) # 輸出:4517720496

? del v1

? v2 = "A"

? print( id(v1) ) # 輸出:4517720496

? # 除此之外,Python內(nèi)部還對字符串做了駐留機(jī)制,針對只含有字母法牲、數(shù)字史汗、下劃線的字符串(見源碼Objects/codeobject.c),如果

? # 內(nèi)存中已存在則不會重新在創(chuàng)建而是使用原來的地址里(不會像free_list那樣一直在內(nèi)存存活拒垃,只有內(nèi)存中有才能被重復(fù)利用)淹办。

? v1 = "asdfg"

? v2 = "asdfg"

? print(id(v1) == id(v2)) # 輸出:True

復(fù)制代碼

list類型,維護(hù)的free_list數(shù)組最多可緩存80個list對象恶复。

v1 = [11,22,33]

print( id(v1) ) # 輸出:4517628816

del v1

v2 = ["你","好"]

print( id(v2) ) # 輸出:4517628816

復(fù)制代碼

tuple類型,維護(hù)一個free_list數(shù)組且數(shù)組容量20速挑,數(shù)組中元素可以是鏈表且每個鏈表最多可以容納2000個元組對象谤牡。元組的free_list數(shù)組在存儲數(shù)據(jù)時,是按照元組可以容納的個數(shù)為索引找到free_list數(shù)組中對應(yīng)的鏈表姥宝,并添加到鏈表中翅萤。

v1 = (1,2)

print( id(v1) )

del v1? # 因元組的數(shù)量為2,所以會把這個對象緩存到free_list[2]的鏈表中腊满。

v2 = ("哈哈哈","Alex")? # 不會重新開辟內(nèi)存套么,而是去free_list[2]對應(yīng)的鏈表中拿到一個對象來使用。

print( id(v2) )

復(fù)制代碼

dict類型碳蛋,維護(hù)的free_list數(shù)組最多可緩存80個dict對象

? v1 = {"k1":123}

? print( id(v1) )? # 輸出:4515998128

? del v1

? v2 = {"name":"哈哈哈","age":18,"gender":"男"}

? print( id(v1) ) # 輸出:4515998128

復(fù)制代碼

C語言源碼底層分析

最后多說一句胚泌,想學(xué)習(xí)Python可聯(lián)系小編,這里有我自己整理的整套python學(xué)習(xí)資料和路線肃弟,想要這些資料的都可以進(jìn)q裙930900780領(lǐng)取玷室。

本文章素材來源于網(wǎng)絡(luò),如有侵權(quán)請聯(lián)系刪除笤受。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末穷缤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子箩兽,更是在濱河造成了極大的恐慌津肛,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汗贫,死亡現(xiàn)場離奇詭異身坐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)芳绩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門掀亥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妥色,你說我怎么就攤上這事搪花。” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵撮竿,是天一觀的道長吮便。 經(jīng)常有香客問我,道長幢踏,這世上最難降的妖魔是什么髓需? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮房蝉,結(jié)果婚禮上僚匆,老公的妹妹穿的比我還像新娘。我一直安慰自己搭幻,他們只是感情好咧擂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著檀蹋,像睡著了一般松申。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俯逾,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天贸桶,我揣著相機(jī)與錄音,去河邊找鬼桌肴。 笑死皇筛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的识脆。 我是一名探鬼主播设联,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼灼捂!你這毒婦竟也來了离例?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悉稠,失蹤者是張志新(化名)和其女友劉穎宫蛆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體的猛,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耀盗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卦尊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叛拷。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖岂却,靈堂內(nèi)的尸體忽然破棺而出忿薇,到底是詐尸還是另有隱情裙椭,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布署浩,位于F島的核電站揉燃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏筋栋。R本人自食惡果不足惜炊汤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弊攘。 院中可真熱鬧抢腐,春花似錦、人聲如沸襟交。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婿着。三九已至,卻和暖如春醋界,著一層夾襖步出監(jiān)牢的瞬間竟宋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工形纺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丘侠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓逐样,卻偏偏與公主長得像蜗字,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子脂新,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359