一、 垃圾回識訓制
1、什么是垃圾回識訓制?
+什么是‘垃圾’?
在Python程式中,‘垃圾’指的是 沒有用處的變數值,
那什么樣的變數值是沒有用的呢?
我們定義變數將變數值存起來的目的是為了以后取出來使用,而取得變數值需要通過其系結的直接參考(如x=10,10被x直接參考)
或 間接參考(如l=[x,],x=10,10被x直接參考,而被容器型別l間接參考),所以當一個變數值不再系結任何參考時,我們就無法再訪問到該變數值了,該變數值就是沒有用的,就應該被當成一個‘垃圾’回收,
解釋器在執行到定義變數的語法時,會申請記憶體空間來存放變數的值,而記憶體的容量是有限的,這就涉及到變數值所占用記憶體空間的回收問題,當一個變數值沒有用了(簡稱垃圾)就應該將其占用的記憶體給回收掉 ,
而記憶體空間的申請與回收是非常耗費精力的事情,而且存在很大的危險性,稍有不慎就有可能引發記憶體溢位問題,好在cpython解釋器提供了自動的垃圾回識訓制來幫我們解決了這件事,
+垃圾回識訓制—Garbage collection(GC)
垃圾回識訓制(簡稱GC)是Python解釋器自帶一種機制,專門用來回收不可用的變數值所占用的記憶體空間,
現在的高級語言如java,c#等,都采用了垃圾收集機制,而不再是c,c++里用戶自己管理維護記憶體的方式,
自己管理記憶體極其自由,可以任意申請記憶體,但如同一把雙刃劍,為大量記憶體泄露,懸空指標等bug埋下隱患,
對于一個字串、串列、類甚至數值都是物件,且定位簡單易用的語言,自然不會讓用戶去處理如何分配回收記憶體的問題,
python里也同java一樣采用了垃圾收集機制,不過不一樣的是: python采用的是參考計數機制為主,標記-清除和分代回收兩種機制為輔的策略,
2、為什么要有垃圾回識訓制?
先來看看專業解釋:
程式運行程序中會申請大量的記憶體空間,而對于一些無用的記憶體空間如果不及時清理的話會導致記憶體使用殆盡(即 記憶體溢位),導致程式崩潰,因此管理記憶體是一件重要且繁雜的事情,而python解釋器自帶的垃圾回識訓制把程式員從繁雜的記憶體管理中解放出來,
(其實就是Python提供給程式員的一個‘偷懶機制’,有了 ‘垃圾回識訓制’ 這個 ‘自動洗塵器’,再也不用被嘲笑“一屋不掃何以掃天下”,)
3、垃圾回識訓制的作業原理
+參考計數(reference counting)——跟蹤和回收垃圾
什么是參考計數?
參考計數就是:變數值被變數名關聯的次數
如:age=18 ,變數值18被關聯了一個變數名age,則稱之為參考計數為1,

參考增加
例如:age=18 (此時,變數值18的參考計數為1)
m=age (把age的記憶體地址給了m, 此時,m,age都關聯了18,所以變數值18的參考計數為2)

參考減少
例如:age=10(名字age先與值18解除關聯,再與3建立了關聯,變數值18的參考計數為1)
del m(del的意思是解除變數名x與變數值18的關聯關系,此時,變數18的參考計數為0)

值18的參考計數一旦變為0,其占用的記憶體地址就應該被解釋器的垃圾回識訓制回收,
即,當一個變數值的參考計數為0時,就成了該被回收的垃圾,其占用的記憶體地址就應該被解釋器的垃圾回識訓制回收,
那問題又來了,垃圾回收有特殊情況嗎?
請繼續聽以下分解——
變數值被關聯次數的增加或減少,都會引發參考計數機制的執行(增加或減少值的參考計數),這存在明顯的效率問題,
此外,參考計數機制還存在著一個致命的弱點,即回圈參考(也稱交叉參考)——容器物件名 . append(值)
容器物件(比如:list,set,dict,class,instance)都可以包含對其他物件的參考,所以都可能產生回圈參考,
# 如下我們定義了兩個串列,簡稱串列1與串列2,變數名l1指向串列1,變數名l2指向串列2 >>> l1=['xxx'] # 串列1被參考一次,串列1的參考計數變為1 >>> l2=['yyy'] # 串列2被參考一次,串列2的參考計數變為1 >>> l1.append(l2) # 把串列2追加到l1中作為第二個元素,串列2的參考計數變為2 >>> l2.append(l1) # 把串列1追加到l2中作為第二個元素,串列1的參考計數變為2 # l1與l2之間有相互參考 # l1 = ['xxx'的記憶體地址,串列2的記憶體地址] # l2 = ['yyy'的記憶體地址,串列1的記憶體地址] >>> l1 ['xxx', ['yyy', [...]]] >>> l2 ['yyy', ['xxx', [...]]] >>> l1[1][1][0] 'xxx'
回圈參考會導致:值不再被任何名字關聯,但是值的參考計數并不會為0,應該被回收但不能被回收 ——
>>> del l1 # 串列1的參考計數減1,串列1的參考計數變為1 >>> del l2 # 串列2的參考計數減1,串列2的參考計數變為1
del 的意思是解除變數名與變數值的關聯關系,處理的是記憶體中的堆疊區,(GC回識訓制處理的是記憶體中的堆區)
此時,只剩下串列1與串列2之間的相互參考,兩個串列的參考計數均不為0,
但兩個串列不再被任何其他物件關聯,沒有任何人可以再參考到它們,所以它倆占用記憶體空間應該被回收,
但由于相互參考的存在,每一個物件的參考計數都不為0,因此這些物件所占用的記憶體永遠不會被釋放,
因此回圈參考是致命的,這與手動進行記憶體管理所產生的記憶體泄露毫無區別,
所以python引入了“標記-清除” 與“分代回收”來分別解決參考計數的回圈參考與效率低的問題,
+標記清除(mark and sweep)——解決容器物件可能產生的回圈參考的問題
標記清除怎么作業,什么時候作業,作業頻率?
在了解標記清除演算法前,我們需要明確一點,關于變數的存盤——
記憶體中有兩塊區域:堆疊區與堆區,在定義變數時,變數名與值記憶體地址的關聯關系存放于堆疊區,變數值存放于堆區,記憶體管理回收的則是堆區的內容,
詳解如下圖:
定義了兩個變數x = 10、y = 20 
當我們執行x=y時,記憶體中的堆疊區與堆區變化如下:

標記/清除演算法的做法是當應用程式可用的記憶體空間被耗盡的時,就會停止整個程式,然后進行兩項作業,第一項則是標記,第二項則是清除,
#標記—標記的程序其實就是,遍歷所有的gc roots物件(堆疊區中的所有內容或者執行緒都可以作為gc roots物件),然后將所有gc roots的物件可以直接或間接訪問到的物件標記為存活的物件,其余的均為非存活物件,應該被清除,
#清除—清除的程序將遍歷堆中所有的物件,將沒有標記的物件(非存活物件)全部清除掉,
直接參考——指的是從堆疊區出發,直接參考到的記憶體地址,
間接參考——指的是從堆疊區出發,參考到堆區后再進一步參考到的記憶體地址,(即本身有個直接參考,再去參考堆區別的記憶體地址)
以我們之前的兩個串列l1與l2為例畫出如下影像:
當我們同時洗掉l1與l2時,會清理到堆疊區中l1與l2的內容:
這樣在啟用標記清除演算法時,發現堆疊區內不再有l1與l2(只剩下堆區內二者的相互參考),于是串列1與串列2都沒有被標記為存活,二者會被清理掉,這樣就解決了回圈參考帶來的記憶體泄漏問題,
形象地理解,就是“順藤摸瓜”——“藤”就像變數名與值記憶體地址的關聯關系, “瓜”就像變數值,如果“藤”已經斷了,那么就把“瓜”抱走吃了吧,不管這個“瓜”有多少兄弟姐妹……
+分代回收(generation collection)—— 以空間換取時間的方式來進一步提高 垃圾回收的效率
為什么要有分代回收?
基于參考計數的回識訓制,每次回收記憶體,都需要把所有物件的參考計數都遍歷一遍,這是非常消耗時間的,于是引入了分代回收來提高回收效率,分代回收采用的是用“空間換時間”的策略,
分代回收的原理:
分代回收的核心思想是:在歷經多次掃描的情況下,都沒有被回收的變數,GC機制就會認為,該變數是常用變數,GC對其掃描的頻率會降低,
具體實作原理如下:
【分代】
分代指的是根據存活時間來為變數劃分不同等級(也就是不同的代)
新定義的變數,放到新生代這個等級中,假設每隔1分鐘掃描新生代一次,
如果發現變數依然被參考,那么該物件的權重(權重本質就是個整數)加一,當變數的權重大于某個設定得值(假設為3),會將它移動到更高一級的青春代,
青春代的gc掃描的頻率低于新生代(掃描時間間隔更長),假設5分鐘掃描青春代一次,這樣每次gc需要掃描的變數的總個數就變少了,節省了掃描的總時間,
接下來,青春代中的物件,也會以同樣的方式被移動到老年代中,也就是等級(代)越高,被垃圾回識訓制掃描的頻率越低,
【回收】
回收依然是使用參考計數作為回收的依據,

雖然分代回收可以起到提升效率的效果,但也存在一定的缺點:
例如:一個變數剛剛從新生代移入青春代,該變數的系結關系就解除了,該變數應該被回收,但青春代的掃描頻率低于新生代,所以該變數的回收就會被延遲,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/183610.html
標籤:Python
上一篇:Python的應用領域與就業前景
