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

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

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

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

1. 什么是垃圾?

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

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

a = 10000

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

a = 30000

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

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

2. 內存泄露和內存溢出

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

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

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

內存溢出:程序在申請內存時,沒有足夠的內存空間供其使用,出現 out of memory

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

3. 引用計數

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

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

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

text = "hello,world"

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

  • 創建字符串對象:它的值是hello,world,
  • 開辟內存空間:在對象進行實例化的時候,解釋器會為對象分配一段內存地址空間。把這個對象的結構體存儲在這段內存地址空間中。

我們再來看看這個對象的結構體

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

熟悉c語言或者c++的朋友,看到這個應該特別熟悉,他就是結構體。這是因為我們Python官方的解釋器是CPython,它底層調用了很多的c類庫與接口。所以一些底層的數據是通過結構體進行存儲的。看不懂的朋友也沒有關系。這里,我們只需要關注一個參數:`ob_refcnt`這個參數非常神奇,它記錄了這個對象的被變量引用的次數。所以上面 hello,world 這個對象的引用計數就是 1,因為現在只有text這個變量引用了它。**①變量初始化賦值:**```pythontext = "hello,world"

②變量引用傳遞:

new_text = text

③刪除第一個變量:

del text

④刪除第二個變量:

del new_text

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

4. 引用計數如何變化

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

4.1 引用計數加一的情況

  • 對象被創建

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

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

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

    func(a)

4.2 引用計數減一

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

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

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

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

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

4.3 查看對象的引用計數

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

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

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

5. 垃圾回收機制

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

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

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

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

5.1 引用計數機制缺點

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

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

缺點:

  1. 需要額外的空間維護引用計數
  2. 無法解決循環引用問題

什么是循環引用問題?看看下面的例子

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

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

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

5.2 標記-清除策略

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

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

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

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

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

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

流程:

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

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

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

5.3 分代回收策略

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

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

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

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

那什么時候會觸發分代回收呢?

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

5.4 gc模塊

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

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

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

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