人生苦短,只談風月,談什么垃圾回收。

據(jù)說上圖是某語言的垃圾回收機制。。。

我們寫過C語言、C++的朋友都知道,我們的C語言是沒有垃圾回收這種說法的。手動分配、釋放內(nèi)存都需要我們的程序員自己完成。不管是“內(nèi)存泄漏” 還是野指針都是讓開發(fā)者非常頭疼的問題。所以C語言開發(fā)這個討論得最多的話題就是內(nèi)存管理了。但是對于其他高級語言來說,例如Java、C#、Python等高級語言,已經(jīng)具備了垃圾回收機制。這樣可以屏蔽內(nèi)存管理的復雜性,使開發(fā)者可以更好的關注核心的業(yè)務邏輯。

對我們的Python開發(fā)者來說,我們可以當甩手掌柜。不用操心它怎么回收程序運行過程中產(chǎn)生的垃圾。但是這畢竟是一門語言的內(nèi)心功法,難道我們甘愿一輩子做一個API調(diào)參俠嗎?

1. 什么是垃圾?

當我們的Python解釋器在執(zhí)行到定義變量的語法時,會申請內(nèi)存空間來存放變量的值,而內(nèi)存的容量是有限的,這就涉及到變量值所占用內(nèi)存空間的回收問題。

當一個對象或者說變量沒有用了,就會被當做“垃圾“。那什么樣的變量是沒有用的呢?

a = 10000

當解釋器執(zhí)行到上面這里的時候,會劃分一塊內(nèi)存來存儲 10000 這個值。此時的 10000 是被變量 a 引用的

a = 30000

當我們修改這個變量的值時,又劃分了一塊內(nèi)存來存 30000 這個值,此時變量a引用的值是30000。

這個時候,我們的 10000 已經(jīng)沒有變量引用它了,我們也可以說它變成了垃圾,但是他依舊占著剛才給他的內(nèi)存。那我們的解釋器,就要把這塊內(nèi)存地盤收回來。

2. 內(nèi)存泄露和內(nèi)存溢出

上面我們了解了什么是程序運行過程中的“垃圾”,那如果,產(chǎn)生了垃圾,我們不去處理,會產(chǎn)生什么樣的后果呢?試想一下,如果你家從不丟垃圾,產(chǎn)生的垃圾就堆在家里會怎么呢?

  1. 家里堆滿垃圾,有個美女想當你對象,但是已經(jīng)沒有空間給她住了。
  2. 你還能住,但是家里的垃圾很占地方,而且很浪費空間,慢慢的,總有一天你的家里會堆滿垃圾

上面的結(jié)果其實就是計算機里面讓所有程序員都聞風喪膽的問題,內(nèi)存溢出和內(nèi)存泄露,輕則導致程序運行速度減慢,重則導致程序崩潰。

內(nèi)存溢出:程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,出現(xiàn) out of memory

內(nèi)存泄露:程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光

3. 引用計數(shù)

前面我們提到過垃圾的產(chǎn)生的是因為,對象沒有再被其他變量引用了。那么,我們的解釋器究竟是怎么知道一個對象還有沒有被引用的呢?

答案就是:引用計數(shù)。python內(nèi)部通過引用計數(shù)機制來統(tǒng)計一個對象被引用的次數(shù)。當這個數(shù)變成0的時候,就說明這個對象沒有被引用了。這個時候它就變成了“垃圾”。

這個引用計數(shù)又是何方神圣呢?讓我們看看代碼

text = "hello,world"

上面的一行代碼做了哪些工作呢?

  • 創(chuàng)建字符串對象:它的值是hello,world,
  • 開辟內(nèi)存空間:在對象進行實例化的時候,解釋器會為對象分配一段內(nèi)存地址空間。把這個對象的結(jié)構(gòu)體存儲在這段內(nèi)存地址空間中。

我們再來看看這個對象的結(jié)構(gòu)體

```c++
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;

熟悉c語言或者c++的朋友,看到這個應該特別熟悉,他就是結(jié)構(gòu)體。這是因為我們Python官方的解釋器是CPython,它底層調(diào)用了很多的c類庫與接口。所以一些底層的數(shù)據(jù)是通過結(jié)構(gòu)體進行存儲的??床欢呐笥岩矝]有關系。這里,我們只需要關注一個參數(shù):`ob_refcnt`這個參數(shù)非常神奇,它記錄了這個對象的被變量引用的次數(shù)。所以上面 hello,world 這個對象的引用計數(shù)就是 1,因為現(xiàn)在只有text這個變量引用了它。**①變量初始化賦值:**```pythontext = "hello,world"

②變量引用傳遞:

new_text = text

③刪除第一個變量:

del text

④刪除第二個變量:

del new_text

此時 "hello,world" 對象的引用計數(shù)為:0,被當成了垃圾。下一步,就該被我們的垃圾回收器給收走了。

4. 引用計數(shù)如何變化

上面我們了解了什么是引用計數(shù)。那這個參數(shù)什么時候會發(fā)生變化呢?

4.1 引用計數(shù)加一的情況

  • 對象被創(chuàng)建

    a = "hello,world"
  • 對象被別的變量引用(賦值給一個變量)

    b = a
  • 對象被作為元素,放在容器中(比如被當作元素放在列表中)

    list = []list.append(a)
  • 對象作為參數(shù)傳遞給函數(shù)

    func(a)

4.2 引用計數(shù)減一

  • 對象的引用變量被顯示銷毀

    del a
  • 對象的引用變量賦值引用其他對象

    a = "hello, Python"   # a的原來的引用對象:a = "hello,world"
  • 對象從容器中被移除,或者容器被銷毀(例:對象從列表中被移除,或者列表被銷毀)

    del list
    list.remove(a)
  • 一個引用離開了它的作用域

    func():  a = "hello,world"  returnfunc()  # 函數(shù)執(zhí)行結(jié)束以后,函數(shù)作用域里面的局部變量a會被釋放

4.3 查看對象的引用計數(shù)

如果要查看對象的引用計數(shù),可以通過內(nèi)置模塊 sys 提供的 getrefcount 方法去查看。

import sysa = "hello,world"print(sys.getrefcount(a))

注意:當使用某個引用作為參數(shù),傳遞給 getrefcount() 時,參數(shù)實際上創(chuàng)建了一個臨時的引用。因此,getrefcount() 所得到的結(jié)果,會比期望的多 1

5. 垃圾回收機制

其實Python的垃圾回收機制,我們前面已經(jīng)說得差不多了。

Python通過引用計數(shù)的方法來說實現(xiàn)垃圾回收,當一個對象的引用計數(shù)為0的時候,就進行垃圾回收。但是如果只使用引用計數(shù)也是有點問題的。所以,python又引進了標記-清除分代收集兩種機制。

Python采用的是引用計數(shù)機制為主,標記-清除和分代收集兩種機制為輔的策略。

前面的引用計數(shù)我們已經(jīng)了解了,那這個標記-清除跟分代收集又是什么呢?

5.1 引用計數(shù)機制缺點

Python語言默認采用的垃圾收集機制是“引用計數(shù)法 ”,該算法最早George E. Collins在1960的時候首次提出,50年后的今天,該算法依然被很多編程語言使用。

引用計數(shù)法:每個對象維護一個 ob_refcnt 字段,用來記錄該對象當前被引用的次數(shù),每當新的引用指向該對象時,它的引用計數(shù)ob_refcnt加1,每當該對象的引用失效時計數(shù)ob_refcnt減1,一旦對象的引用計數(shù)為0,該對象立即被回收,對象占用的內(nèi)存空間將被釋放。

缺點:

  1. 需要額外的空間維護引用計數(shù)
  2. 無法解決循環(huán)引用問題

什么是循環(huán)引用問題?看看下面的例子

a = {"key":"a"}  # 字典對象a的引用計數(shù):1b = {"key":"b"}  # 字典對象b的引用計數(shù):1a["b"] = b  # 字典對象b的引用計數(shù):2b["a"] = a  # 字典對象a的引用計數(shù):2del a  # 字典對象a的引用計數(shù):1del b  # 字典對象b的引用計數(shù):1

看上面的例子,明明兩個變量都刪除了,但是這兩個對象卻沒有得到釋放。原因是他們的引用計數(shù)都沒有減少到0。而我們垃圾回收機制只有當引用計數(shù)為0的時候才會釋放對象。這是一個無法解決的致命問題。這兩個對象始終不會被銷毀,這樣就會導致內(nèi)存泄漏。

那怎么解決這個問題呢?這個時候 標記-清除 就排上了用場。標記清除可以處理這種循環(huán)引用的情況。

5.2 標記-清除策略

Python采用了標記-清除策略,解決容器對象可能產(chǎn)生的循環(huán)引用問題。

該策略在進行垃圾回收時分成了兩步,分別是:

  • 標記階段,遍歷所有的對象,如果是可達的(reachable),也就是還有對象引用它,那么就標記該對象為可達;
  • 清除階段,再次遍歷對象,如果發(fā)現(xiàn)某個對象沒有標記為可達,則就將其回收

這里簡單介紹一下標記-清除策略的流程

可達(活動)對象:從root集合節(jié)點有(通過鏈式引用)路徑達到的對象節(jié)點

不可達(非活動)對象:從root集合節(jié)點沒有(通過鏈式引用)路徑到達的對象節(jié)點

流程:

  1. 首先,從root集合節(jié)點出發(fā),沿著有向邊遍歷所有的對象節(jié)點
  2. 對每個對象分別標記可達對象還是不可達對象
  3. 再次遍歷所有節(jié)點,對所有標記為不可達的對象進行垃圾回收、銷毀。

標記-清除是一種周期性策略,相當于是一個定時任務,每隔一段時間進行一次掃描。

并且標記-清除工作時會暫停整個應用程序,等待標記清除結(jié)束后才會恢復應用程序的運行。

5.3 分代回收策略

分代回收建立標記清除的基礎之上,因為我們的標記-清除策略會將我們的程序阻塞。為了減少應用程序暫停的時間,Python 通過“分代回收”(Generational Collection)策略。以空間換時間的方法提高垃圾回收效率。

分代的垃圾收集技術是在上個世紀 80 年代初發(fā)展起來的一種垃圾收集機制。

簡單來說就是:對象存在時間越長,越可能不是垃圾,應該越少去收集

Python 將內(nèi)存根據(jù)對象的存活時間劃分為不同的集合,每個集合稱為一個代,Python 將內(nèi)存分為了 3“代”,分別為年輕代(第 0 代)、中年代(第 1 代)、老年代(第 2 代)。

那什么時候會觸發(fā)分代回收呢?

import gcprint(gc.get_threshold())# (700, 10, 10)# 上面這個是默認的回收策略的閾值# 也可以自己設置回收策略的閾值gc.set_threshold(500, 5, 5)
  • 700:表示當分配對象的個數(shù)達到700時,進行一次0代回收
  • 10:當進行10次0代回收以后觸發(fā)一次1代回收
  • 10:當進行10次1代回收以后觸發(fā)一次2代回收

5.4 gc模塊

  • gc.get_count():獲取當前自動執(zhí)行垃圾回收的計數(shù)器,返回一個長度為3的列表
  • gc.get_threshold():獲取gc模塊中自動執(zhí)行垃圾回收的頻率,默認是(700, 10, 10)
  • gc.set_threshold(threshold0[,threshold1,threshold2]):設置自動執(zhí)行垃圾回收的頻率
  • gc.disable():python3默認開啟gc機制,可以使用該方法手動關閉gc機制
  • gc.collect():手動調(diào)用垃圾回收機制回收垃圾

其實,既然我們選擇了python,性能就不是最重要的了。我相信大部分的python工程師甚至都還沒遇到過性能問題,因為現(xiàn)在的機器性能可以彌補。而對于內(nèi)存管理與垃圾回收,python提供了甩手掌柜的方式讓我們更關注業(yè)務層,這不是更加符合人生苦短,我用python的理念么。如果我還需要像C++那樣小心翼翼的進行內(nèi)存的管理,那我為什么還要用python呢?咱不就是圖他的便利嘛。所以,放心去干吧。越早下班越好!

創(chuàng)作不易,且讀且珍惜。如有錯漏還請海涵并聯(lián)系作者修改,內(nèi)容有參考,如有侵權,請聯(lián)系作者刪除。如果文章對您有幫助,還請動動小手,您的支持是我最大的動力。

關注小編公眾號:偷偷學習,卷死他們