Python語法入門之垃圾回收機制

Python語法入門之垃圾回收機制

1、引入

解釋器在執行到定義變量的語法時,而內存空間是有限的,而內存的容量是有限的,這就涉及到變量值所佔用內存空間的回收問題,當一個變量值沒有用了(簡稱垃圾)就應該將其佔用的內存給回收掉,那什麼樣的變量值是沒有用的呢?python

單從邏輯層面分析,咱們定義變量將變量值存起來的目的是爲了之後取出來使用,而取得變量值須要經過其綁定的直接引用(如x=10,10被x直接引用)或間接引用(如l=[x,],x=10,10被x直接引用,而被容器類型l間接引用),因此當一個變量值再也不綁定任何引用時,咱們就沒法再訪問到該變量值了,該變量值天然就是沒有用的,就應該被當成一個垃圾回收。程序員

毫無疑問,內存空間的申請與回收都是很是耗費精力的事情,並且存在很大的危險性,稍有不慎就有可能引起內存溢出問題,好在Cpython解釋器提供了自動的垃圾回收機制來幫咱們解決了這件事。算法

2、什麼是垃圾回收機制?

程序運行過程當中會申請大量的內存空間,而對於一些無用的內存空間若是不及時清理的話會致使內存使用殆盡(內存溢出),致使程序崩潰。markdown

所以管理內存是一件重要且繁雜的事情,而python解釋器自帶的垃圾回收機制把程序員從繁雜的內存管理中解放出來。app

3、理解GC原理須要儲備知識

堆區與棧區

在定義變量時,變量名與變量值都是須要存儲的,分別對應內存中的兩塊區域:堆區與棧區。ide

  1. 變量名與值內存地址的關聯關係存放於棧區
  2. 變量值存放於堆區,內存管理回收的則是堆區的內容

例如,咱們定義了兩個變量x = 十、y = 20,詳解以下圖,atom

當咱們執行x=y時,內存中的棧區與堆區變化以下spa

1605662988(1)

直接引用與間接引用

  • 直接引用指的是從棧區出發直接引用到的內存地址。線程

  • 間接引用指的是從棧區出發引用到堆區後,再經過進一步引用才能到達的內存地址。code

好比:

l2 = [20, 30]  # 列表自己被變量名l2直接引用,包含的元素被列表間接引用
x = 10  # 值10被變量名x直接引用
l1 = [x, l2]  # 列表自己被變量名l1直接引用,包含的元素被列表間接引用

圖解以下:

img

4、垃圾回收機制原理分析

Python的GC模塊主要運用了引用計數(reference counting)來跟蹤和回收垃圾

在引用計數的基礎上,還能夠經過標記-清除(mark and sweep)解決容器對象可能產生的循環引用的問題,而且經過分代回收(generation collection)以空間換取時間的方式來進一步提升垃圾回收的效率。

引用計數

引用計數就是:變量值被變量名關聯的次數

如:age=18

變量值18被關聯了一個變量名age,稱之爲引用計數爲1

img

引用計數增長:

age=18 (此時,變量值18的引用計數爲1)

m=age (把age的內存地址給了m,此時,m,age都關聯了18,因此變量值18的引用計數爲2)

img

引用計數減小:

age=10(名字age先與值18解除關聯,再與3創建了關聯,變量值18的引用計數爲1)

del m(del的意思是解除變量名x與變量值18的關聯關係,此時,變量18的引用計數爲0)

img

值18的引用計數一旦變爲0,其佔用的內存地址就應該被解釋器的垃圾回收機制回收

5、引用計數的問題與解決方案

1.問題一:循環引用

​ 引用計數機制存在着一個致命的弱點,即循環引用(也稱交叉引用)

# 以下咱們定義了兩個列表,簡稱列表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'

img

循環引用會致使:值再也不被任何名字關聯,可是值的引用計數並不會爲0,應該被回收但不能被回收,什麼意思呢?試想一下,請看以下操做

>>> del l1 # 列表1的引用計數減1,列表1的引用計數變爲1
>>> del l2 # 列表2的引用計數減1,列表2的引用計數變爲1

此時,只剩下列表1與列表2之間的相互引用

img

但此時兩個列表的引用計數均不爲0,但兩個列表再也不被任何其餘對象關聯,沒有任何人能夠再引用到它們,因此它倆佔用內存空間應該被回收,但因爲相互引用的存在,每個對象的引用計數都不爲0,所以這些對象所佔用的內存永遠不會被釋放,因此循環引用是致命的,這與手動進行內存管理所產生的內存泄露毫無區別。 因此Python引入了「標記-清除」 與「分代回收」來分別解決引用計數的循環引用與效率低的問題

解決方案:標記-清除

​ 容器對象(好比:list,set,dict,class,instance)均可以包含對其餘對象的引用,因此均可能產生循環引用。而「標記-清除」計數就是爲了解決循環引用的問題。

​ 標記/清除算法的作法是當應用程序可用的內存空間被耗盡的時,就會中止整個程序,而後進行兩項工做,第一項則是標記,第二項則是清除

  1. 標記

    通俗地講就是:棧區至關於「根」,凡是從根出發能夠訪達(直接或間接引用)的,都稱之爲「有根之人」,有根之人當活,無根之人當死。

    具體地:標記的過程其實就是,遍歷全部的GC Roots對象(棧區中的全部內容或者線程均可以做爲GC Roots對象),而後將全部GC Roots的對象能夠直接或間接訪問到的對象標記爲存活的對象,其他的均爲非存活對象,應該被清除。

  2. 清除

    清除的過程將遍歷堆中全部的對象,將沒有標記的對象所有清除掉。

基於上例的循環引用,當咱們同時刪除l1與l2時,會清理到棧區中l1與l2的內容以及直接引用關係

img

這樣在啓用標記清除算法時,從棧區出發,沒有任何一條直接或間接引用能夠訪達l1與l2,即l1與l2成了「無根之人」,因而l1與l2都沒有被標記爲存活,兩者會被清理掉,這樣就解決了循環引用帶來的內存泄漏問題。

2.問題二:效率問題

基於引用計數的回收機制,每次回收內存,都須要把全部對象的引用計數都遍歷一遍,這是很是消耗時間的,因而引入了分代回收來提升回收效率,分代回收採用的是用「空間換時間」的策略。

解決方案:分代回收

分代:

分代回收的核心思想是:在歷經屢次掃描的狀況下,都沒有被回收的變量,gc機制就會認爲,該變量是經常使用變量,gc對其掃描的頻率會下降,具體實現原理以下:

分代指的是根據存活時間來爲變量劃分不一樣等級(也就是不一樣的代)

新定義的變量,放到新生代這個等級中,假設每隔1分鐘掃描新生代一次,若是發現變量依然被引用,那麼該對象的權重(權重本質就是個整數)加一,當變量的權重大於某個設定得值(假設爲3),會將它移動到更高一級的青春代,青春代的gc掃描的頻率低於新生代(掃描時間間隔更長),假設5分鐘掃描青春代一次,這樣每次gc須要掃描的變量的總個數就變少了,節省了掃描的總時間,接下來,青春代中的對象,也會以一樣的方式被移動到老年代中。也就是等級(代)越高,被垃圾回收機制掃描的頻率越低。

回收:

回收依然是使用引用計數做爲回收的依據

未命名文件(4)

雖然分代回收能夠起到提高效率的效果,但也存在必定的缺點:

例如一個變量剛剛重新生代移入青春代,該變量的綁定關係就解除了,該變量應該被回收,但青春代的掃描頻率低於新生代,這就到致使了應該被回收的垃圾沒有獲得及時地清理。

沒有十全十美的方案:
毫無疑問,若是沒有分代回收,即引用計數機制一直不停地對全部變量進行全體掃描,能夠更及時地清理掉垃圾佔用的內存,但這種一直不停地對全部變量進行全體掃描的方式效率極低,因此咱們只能將兩者中和。

綜上

垃圾回收機制是在清理垃圾&釋放內存的大背景下,容許分代回收以極小部分垃圾不會被及時釋放爲代價,以此換取。

6、總結:

引用計數

x = 18  #18被引用了一次,計數爲1
y = x   #18 被引用了兩次,計數爲2

循環引用

循環引用----->存在着內存泄漏的漏洞, 須要清除標記

分代回收

y = 20  #18的引用減1,計數爲1
del x   #18的引用減1,計數爲0
相關文章
相關標籤/搜索