人生苦短,只談風月,談什么垃圾回收。
據說上圖是某語言的垃圾回收機制。。。
我們寫過C語言、C++的朋友都知道,我們的C語言是沒有垃圾回收這種說法的。手動分配、釋放內存都需要我們的程序員自己完成。不管是“內存泄漏” 還是野指針都是讓開發者非常頭疼的問題。所以C語言開發這個討論得最多的話題就是內存管理了。但是對于其他高級語言來說,例如JAVA、C#、Python/ target=_blank class=infotextkey>Python等高級語言,已經具備了垃圾回收機制。這樣可以屏蔽內存管理的復雜性,使開發者可以更好地關注核心的業務邏輯。
對我們的Python開發者來說,我們可以當甩手掌柜。不用操心它怎么回收程序運行過程中產生的垃圾。但是這畢竟是一門語言的內心功法,難道我們甘愿一輩子做一個API調參俠嗎?
1. 什么是垃圾?
當我們的Python解釋器在執行到定義變量的語法時,會申請內存空間來存放變量的值,而內存的容量是有限的,這就涉及到變量值所占用內存空間的回收問題。
當一個對象或者說變量沒有用了,就會被當做“垃圾“。那什么樣的變量是沒有用的呢?
a = 10000
當解釋器執行到上面這里的時候,會劃分一塊內存來存儲 10000 這個值。此時的 10000 是被變量 a 引用的
a = 30000
當我們修改這個變量的值時,又劃分了一塊內存來存 30000 這個值,此時變量a引用的值是30000。
這個時候,我們的 10000 已經沒有變量引用它了,我們也可以說它變成了垃圾,但是他依舊占著剛才給他的內存。那我們的解釋器,就要把這塊內存地盤收回來。
2. 內存泄露和內存溢出
上面我們了解了什么是程序運行過程中的“垃圾”,那如果,產生了垃圾,我們不去處理,會產生什么樣的后果呢?試想一下,如果你家從不丟垃圾,產生的垃圾就堆在家里會怎么樣呢?
- 家里堆滿垃圾,有個美女想當你的對象,但是已經沒有空間給她住了。
- 你還能住,但是家里的垃圾很占地方,而且很浪費空間,慢慢的,總有一天你的家里會堆滿垃圾
上面的結果其實就是計算機里面讓所有程序員都聞風喪膽的問題,內存溢出和內存泄露,輕則導致程序運行速度減慢,重則導致程序崩潰。
內存溢出:程序在申請內存時,沒有足夠的內存空間供其使用,出現 out of memory
內存泄露:程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光
3. 引用計數
前面我們提到過垃圾的產生是因為,對象沒有再被其他變量引用了。那么,我們的解釋器究竟是怎么知道一個對象還有沒有被引用的呢?
答案就是:引用計數。python內部通過引用計數機制來統計一個對象被引用的次數。當這個數變成0的時候,就說明這個對象沒有被引用了。這個時候它就變成了“垃圾”。
這個引用計數又是何方神圣呢?讓我們看看代碼
text = "hello,world"
上面的一行代碼做了哪些工作呢?
- 創建字符串對象:它的值是hello,world,
- 開辟內存空間:在對象進行實例化的時候,解釋器會為對象分配一段內存地址空間。把這個對象的結構體存儲在這段內存地址空間中。
我們再來看看這個對象的結構體
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
熟悉c語言或者c++的朋友,看到這個應該特別熟悉,他就是結構體。這是因為我們Python官方的解釋器是CPython,它底層調用了很多的c類庫與接口。所以一些底層的數據是通過結構體進行存儲的。看不懂的朋友也沒有關系。
這里,我們只需要關注一個參數:ob_refcnt
這個參數非常神奇,它記錄了這個對象的被變量引用的次數。所以上面 hello,world 這個對象的引用計數就是 1,因為現在只有text這個變量引用了它。
①變量初始化賦值:
text = "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"
return
func() # 函數執行結束以后,函數作用域里面的局部變量a會被釋放
4.3 查看對象的引用計數
如果要查看對象的引用計數,可以通過內置模塊 sys 提供的 getrefcount 方法去查看。
import sys
a = "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,該對象立即被回收,對象占用的內存空間將被釋放。
缺點:
- 需要額外的空間維護引用計數
- 無法解決循環引用問題
什么是循環引用問題?看看下面的例子
a = {"key":"a"} # 字典對象a的引用計數:1
b = {"key":"b"} # 字典對象b的引用計數:1
a["b"] = b # 字典對象b的引用計數:2
b["a"] = a # 字典對象a的引用計數:2
del a # 字典對象a的引用計數:1
del b # 字典對象b的引用計數:1
看上面的例子,明明兩個變量都刪除了,但是這兩個對象卻沒有得到釋放。原因是他們的引用計數都沒有減少到0。而我們垃圾回收機制只有當引用計數為0的時候才會釋放對象。這是一個無法解決的致命問題。這兩個對象始終不會被銷毀,這樣就會導致內存泄漏。
那怎么解決這個問題呢?這個時候 標記-清除 就排上了用場。標記清除可以處理這種循環引用的情況。
5.2 標記-清除策略
Python采用了標記-清除策略,解決容器對象可能產生的循環引用問題。
該策略在進行垃圾回收時分成了兩步,分別是:
- 標記階段,遍歷所有的對象,如果是可達的(reachable),也就是還有對象引用它,那么就標記該對象為可達;
- 清除階段,再次遍歷對象,如果發現某個對象沒有標記為可達,則就將其回收
這里簡單介紹一下標記-清除策略的流程
可達(活動)對象:從root集合節點有(通過鏈式引用)路徑達到的對象節點
不可達(非活動)對象:從root集合節點沒有(通過鏈式引用)路徑到達的對象節點
流程:
- 首先,從root集合節點出發,沿著有向邊遍歷所有的對象節點
- 對每個對象分別標記可達對象還是不可達對象
- 再次遍歷所有節點,對所有標記為不可達的對象進行垃圾回收、銷毀。
標記-清除是一種周期性策略,相當于是一個定時任務,每隔一段時間進行一次掃描。
并且標記-清除工作時會暫停整個應用程序,等待標記清除結束后才會恢復應用程序的運行。
5.3 分代回收策略
分代回收建立標記清除的基礎之上,因為我們的標記-清除策略會將我們的程序阻塞。
簡單來說就是:對象存在時間越長,越可能不是垃圾,應該越少去收集
那什么時候會觸發分代回收呢?
import gc
print(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呢?咱不就是圖他的便利嘛。所以,放心去干吧!
原文鏈接:
https://blog.51cto.com/u_14666251/4674779