人生苦短,只談風(fēng)月,談什么垃圾回收。
據(jù)說上圖是某語言的垃圾回收機(jī)制。。。
我們寫過C語言、C++的朋友都知道,我們的C語言是沒有垃圾回收這種說法的。手動(dòng)分配、釋放內(nèi)存都需要我們的程序員自己完成。不管是“內(nèi)存泄漏” 還是野指針都是讓開發(fā)者非常頭疼的問題。所以C語言開發(fā)這個(gè)討論得最多的話題就是內(nèi)存管理了。但是對(duì)于其他高級(jí)語言來說,例如Java、C#、Python等高級(jí)語言,已經(jīng)具備了垃圾回收機(jī)制。這樣可以屏蔽內(nèi)存管理的復(fù)雜性,使開發(fā)者可以更好的關(guān)注核心的業(yè)務(wù)邏輯。
對(duì)我們的Python開發(fā)者來說,我們可以當(dāng)甩手掌柜。不用操心它怎么回收程序運(yùn)行過程中產(chǎn)生的垃圾。但是這畢竟是一門語言的內(nèi)心功法,難道我們甘愿一輩子做一個(gè)API調(diào)參俠嗎?
1. 什么是垃圾?
當(dāng)我們的Python解釋器在執(zhí)行到定義變量的語法時(shí),會(huì)申請(qǐng)內(nèi)存空間來存放變量的值,而內(nèi)存的容量是有限的,這就涉及到變量值所占用內(nèi)存空間的回收問題。
當(dāng)一個(gè)對(duì)象或者說變量沒有用了,就會(huì)被當(dāng)做“垃圾“。那什么樣的變量是沒有用的呢?
a = 10000
當(dāng)解釋器執(zhí)行到上面這里的時(shí)候,會(huì)劃分一塊內(nèi)存來存儲(chǔ) 10000 這個(gè)值。此時(shí)的 10000 是被變量 a 引用的
a = 30000
當(dāng)我們修改這個(gè)變量的值時(shí),又劃分了一塊內(nèi)存來存 30000 這個(gè)值,此時(shí)變量a引用的值是30000。
這個(gè)時(shí)候,我們的 10000 已經(jīng)沒有變量引用它了,我們也可以說它變成了垃圾,但是他依舊占著剛才給他的內(nèi)存。那我們的解釋器,就要把這塊內(nèi)存地盤收回來。
2. 內(nèi)存泄露和內(nèi)存溢出
上面我們了解了什么是程序運(yùn)行過程中的“垃圾”,那如果,產(chǎn)生了垃圾,我們不去處理,會(huì)產(chǎn)生什么樣的后果呢?試想一下,如果你家從不丟垃圾,產(chǎn)生的垃圾就堆在家里會(huì)怎么呢?
- 家里堆滿垃圾,有個(gè)美女想當(dāng)你對(duì)象,但是已經(jīng)沒有空間給她住了。
- 你還能住,但是家里的垃圾很占地方,而且很浪費(fèi)空間,慢慢的,總有一天你的家里會(huì)堆滿垃圾
上面的結(jié)果其實(shí)就是計(jì)算機(jī)里面讓所有程序員都聞風(fēng)喪膽的問題,內(nèi)存溢出和內(nèi)存泄露,輕則導(dǎo)致程序運(yùn)行速度減慢,重則導(dǎo)致程序崩潰。
內(nèi)存溢出:程序在申請(qǐng)內(nèi)存時(shí),沒有足夠的內(nèi)存空間供其使用,出現(xiàn) out of memory
內(nèi)存泄露:程序在申請(qǐng)內(nèi)存后,無法釋放已申請(qǐng)的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略,但內(nèi)存泄露堆積后果很嚴(yán)重,無論多少內(nèi)存,遲早會(huì)被占光
3. 引用計(jì)數(shù)
前面我們提到過垃圾的產(chǎn)生的是因?yàn)椋瑢?duì)象沒有再被其他變量引用了。那么,我們的解釋器究竟是怎么知道一個(gè)對(duì)象還有沒有被引用的呢?
答案就是:引用計(jì)數(shù)。python內(nèi)部通過引用計(jì)數(shù)機(jī)制來統(tǒng)計(jì)一個(gè)對(duì)象被引用的次數(shù)。當(dāng)這個(gè)數(shù)變成0的時(shí)候,就說明這個(gè)對(duì)象沒有被引用了。這個(gè)時(shí)候它就變成了“垃圾”。
這個(gè)引用計(jì)數(shù)又是何方神圣呢?讓我們看看代碼
text = "hello,world"
上面的一行代碼做了哪些工作呢?
- 創(chuàng)建字符串對(duì)象:它的值是hello,world,
- 開辟內(nèi)存空間:在對(duì)象進(jìn)行實(shí)例化的時(shí)候,解釋器會(huì)為對(duì)象分配一段內(nèi)存地址空間。把這個(gè)對(duì)象的結(jié)構(gòu)體存儲(chǔ)在這段內(nèi)存地址空間中。
我們?cè)賮砜纯催@個(gè)對(duì)象的結(jié)構(gòu)體
```c++
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
熟悉c語言或者c++的朋友,看到這個(gè)應(yīng)該特別熟悉,他就是結(jié)構(gòu)體。這是因?yàn)槲覀働ython官方的解釋器是CPython,它底層調(diào)用了很多的c類庫與接口。所以一些底層的數(shù)據(jù)是通過結(jié)構(gòu)體進(jìn)行存儲(chǔ)的。看不懂的朋友也沒有關(guān)系。這里,我們只需要關(guān)注一個(gè)參數(shù):`ob_refcnt`這個(gè)參數(shù)非常神奇,它記錄了這個(gè)對(duì)象的被變量引用的次數(shù)。所以上面 hello,world 這個(gè)對(duì)象的引用計(jì)數(shù)就是 1,因?yàn)楝F(xiàn)在只有text這個(gè)變量引用了它。**①變量初始化賦值:**```pythontext = "hello,world"
②變量引用傳遞:
new_text = text
③刪除第一個(gè)變量:
del text
④刪除第二個(gè)變量:
del new_text
此時(shí) "hello,world" 對(duì)象的引用計(jì)數(shù)為:0,被當(dāng)成了垃圾。下一步,就該被我們的垃圾回收器給收走了。
4. 引用計(jì)數(shù)如何變化
上面我們了解了什么是引用計(jì)數(shù)。那這個(gè)參數(shù)什么時(shí)候會(huì)發(fā)生變化呢?
4.1 引用計(jì)數(shù)加一的情況
對(duì)象被創(chuàng)建
a = "hello,world"
對(duì)象被別的變量引用(賦值給一個(gè)變量)
b = a
對(duì)象被作為元素,放在容器中(比如被當(dāng)作元素放在列表中)
list = []list.append(a)
對(duì)象作為參數(shù)傳遞給函數(shù)
func(a)
4.2 引用計(jì)數(shù)減一
對(duì)象的引用變量被顯示銷毀
del a
對(duì)象的引用變量賦值引用其他對(duì)象
a = "hello, Python" # a的原來的引用對(duì)象:a = "hello,world"
對(duì)象從容器中被移除,或者容器被銷毀(例:對(duì)象從列表中被移除,或者列表被銷毀)
del list
list.remove(a)
一個(gè)引用離開了它的作用域
func(): a = "hello,world" returnfunc() # 函數(shù)執(zhí)行結(jié)束以后,函數(shù)作用域里面的局部變量a會(huì)被釋放
4.3 查看對(duì)象的引用計(jì)數(shù)
如果要查看對(duì)象的引用計(jì)數(shù),可以通過內(nèi)置模塊 sys 提供的 getrefcount
方法去查看。
import sysa = "hello,world"print(sys.getrefcount(a))
注意:當(dāng)使用某個(gè)引用作為參數(shù),傳遞給 getrefcount() 時(shí),參數(shù)實(shí)際上創(chuàng)建了一個(gè)臨時(shí)的引用。因此,getrefcount() 所得到的結(jié)果,會(huì)比期望的多 1
5. 垃圾回收機(jī)制
其實(shí)Python的垃圾回收機(jī)制,我們前面已經(jīng)說得差不多了。
Python通過引用計(jì)數(shù)的方法來說實(shí)現(xiàn)垃圾回收,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為0的時(shí)候,就進(jìn)行垃圾回收。但是如果只使用引用計(jì)數(shù)也是有點(diǎn)問題的。所以,python又引進(jìn)了標(biāo)記-清除和分代收集兩種機(jī)制。
Python采用的是引用計(jì)數(shù)機(jī)制為主,標(biāo)記-清除和分代收集兩種機(jī)制為輔的策略。
前面的引用計(jì)數(shù)我們已經(jīng)了解了,那這個(gè)標(biāo)記-清除跟分代收集又是什么呢?
5.1 引用計(jì)數(shù)機(jī)制缺點(diǎn)
Python語言默認(rèn)采用的垃圾收集機(jī)制是“引用計(jì)數(shù)法 ”,該算法最早George E. Collins在1960的時(shí)候首次提出,50年后的今天,該算法依然被很多編程語言使用。
引用計(jì)數(shù)法:每個(gè)對(duì)象維護(hù)一個(gè)
ob_refcnt
字段,用來記錄該對(duì)象當(dāng)前被引用的次數(shù),每當(dāng)新的引用指向該對(duì)象時(shí),它的引用計(jì)數(shù)ob_refcnt
加1,每當(dāng)該對(duì)象的引用失效時(shí)計(jì)數(shù)ob_refcnt
減1,一旦對(duì)象的引用計(jì)數(shù)為0,該對(duì)象立即被回收,對(duì)象占用的內(nèi)存空間將被釋放。
缺點(diǎn):
- 需要額外的空間維護(hù)引用計(jì)數(shù)
- 無法解決循環(huán)引用問題
什么是循環(huán)引用問題?看看下面的例子
a = {"key":"a"} # 字典對(duì)象a的引用計(jì)數(shù):1b = {"key":"b"} # 字典對(duì)象b的引用計(jì)數(shù):1a["b"] = b # 字典對(duì)象b的引用計(jì)數(shù):2b["a"] = a # 字典對(duì)象a的引用計(jì)數(shù):2del a # 字典對(duì)象a的引用計(jì)數(shù):1del b # 字典對(duì)象b的引用計(jì)數(shù):1
看上面的例子,明明兩個(gè)變量都刪除了,但是這兩個(gè)對(duì)象卻沒有得到釋放。原因是他們的引用計(jì)數(shù)都沒有減少到0。而我們垃圾回收機(jī)制只有當(dāng)引用計(jì)數(shù)為0的時(shí)候才會(huì)釋放對(duì)象。這是一個(gè)無法解決的致命問題。這兩個(gè)對(duì)象始終不會(huì)被銷毀,這樣就會(huì)導(dǎo)致內(nèi)存泄漏。
那怎么解決這個(gè)問題呢?這個(gè)時(shí)候 標(biāo)記-清除 就排上了用場(chǎng)。標(biāo)記清除可以處理這種循環(huán)引用的情況。
5.2 標(biāo)記-清除策略
Python采用了標(biāo)記-清除策略,解決容器對(duì)象可能產(chǎn)生的循環(huán)引用問題。
該策略在進(jìn)行垃圾回收時(shí)分成了兩步,分別是:
- 標(biāo)記階段,遍歷所有的對(duì)象,如果是可達(dá)的(reachable),也就是還有對(duì)象引用它,那么就標(biāo)記該對(duì)象為可達(dá);
- 清除階段,再次遍歷對(duì)象,如果發(fā)現(xiàn)某個(gè)對(duì)象沒有標(biāo)記為可達(dá),則就將其回收
這里簡(jiǎn)單介紹一下標(biāo)記-清除策略的流程
可達(dá)(活動(dòng))對(duì)象:從root集合節(jié)點(diǎn)有(通過鏈?zhǔn)揭茫┞窂竭_(dá)到的對(duì)象節(jié)點(diǎn)
不可達(dá)(非活動(dòng))對(duì)象:從root集合節(jié)點(diǎn)沒有(通過鏈?zhǔn)揭茫┞窂降竭_(dá)的對(duì)象節(jié)點(diǎn)
流程:
- 首先,從root集合節(jié)點(diǎn)出發(fā),沿著有向邊遍歷所有的對(duì)象節(jié)點(diǎn)
- 對(duì)每個(gè)對(duì)象分別標(biāo)記可達(dá)對(duì)象還是不可達(dá)對(duì)象
- 再次遍歷所有節(jié)點(diǎn),對(duì)所有標(biāo)記為不可達(dá)的對(duì)象進(jìn)行垃圾回收、銷毀。
標(biāo)記-清除是一種周期性策略,相當(dāng)于是一個(gè)定時(shí)任務(wù),每隔一段時(shí)間進(jìn)行一次掃描。
并且標(biāo)記-清除工作時(shí)會(huì)暫停整個(gè)應(yīng)用程序,等待標(biāo)記清除結(jié)束后才會(huì)恢復(fù)應(yīng)用程序的運(yùn)行。
5.3 分代回收策略
分代回收建立標(biāo)記清除的基礎(chǔ)之上,因?yàn)槲覀兊臉?biāo)記-清除策略會(huì)將我們的程序阻塞。為了減少應(yīng)用程序暫停的時(shí)間,Python 通過“分代回收”(Generational Collection)策略。以空間換時(shí)間的方法提高垃圾回收效率。
分代的垃圾收集技術(shù)是在上個(gè)世紀(jì) 80 年代初發(fā)展起來的一種垃圾收集機(jī)制。
簡(jiǎn)單來說就是:對(duì)象存在時(shí)間越長(zhǎng),越可能不是垃圾,應(yīng)該越少去收集
Python 將內(nèi)存根據(jù)對(duì)象的存活時(shí)間劃分為不同的集合,每個(gè)集合稱為一個(gè)代,Python 將內(nèi)存分為了 3“代”,分別為年輕代(第 0 代)、中年代(第 1 代)、老年代(第 2 代)。
那什么時(shí)候會(huì)觸發(fā)分代回收呢?
import gcprint(gc.get_threshold())# (700, 10, 10)# 上面這個(gè)是默認(rèn)的回收策略的閾值# 也可以自己設(shè)置回收策略的閾值gc.set_threshold(500, 5, 5)
- 700:表示當(dāng)分配對(duì)象的個(gè)數(shù)達(dá)到700時(shí),進(jìn)行一次0代回收
- 10:當(dāng)進(jìn)行10次0代回收以后觸發(fā)一次1代回收
- 10:當(dāng)進(jìn)行10次1代回收以后觸發(fā)一次2代回收
5.4 gc模塊
- gc.get_count():獲取當(dāng)前自動(dòng)執(zhí)行垃圾回收的計(jì)數(shù)器,返回一個(gè)長(zhǎng)度為3的列表
- gc.get_threshold():獲取gc模塊中自動(dòng)執(zhí)行垃圾回收的頻率,默認(rèn)是(700, 10, 10)
- gc.set_threshold(threshold0[,threshold1,threshold2]):設(shè)置自動(dòng)執(zhí)行垃圾回收的頻率
- gc.disable():python3默認(rèn)開啟gc機(jī)制,可以使用該方法手動(dòng)關(guān)閉gc機(jī)制
- gc.collect():手動(dòng)調(diào)用垃圾回收機(jī)制回收垃圾
其實(shí),既然我們選擇了python,性能就不是最重要的了。我相信大部分的python工程師甚至都還沒遇到過性能問題,因?yàn)楝F(xiàn)在的機(jī)器性能可以彌補(bǔ)。而對(duì)于內(nèi)存管理與垃圾回收,python提供了甩手掌柜的方式讓我們更關(guān)注業(yè)務(wù)層,這不是更加符合人生苦短,我用python的理念么。如果我還需要像C++那樣小心翼翼的進(jìn)行內(nèi)存的管理,那我為什么還要用python呢?咱不就是圖他的便利嘛。所以,放心去干吧。越早下班越好!
創(chuàng)作不易,且讀且珍惜。如有錯(cuò)漏還請(qǐng)海涵并聯(lián)系作者修改,內(nèi)容有參考,如有侵權(quán),請(qǐng)聯(lián)系作者刪除。如果文章對(duì)您有幫助,還請(qǐng)動(dòng)動(dòng)小手,您的支持是我最大的動(dòng)力。
關(guān)注小編公眾號(hào):偷偷學(xué)習(xí),卷死他們