天天看點

Python垃圾回收機制

知識點的鋪墊

  對象和引用

    python作為一門動态語言,一個簡單的指派語句也是很值得研究的,重要特點就是引用對象分離。

a = 1
      

    其中整數1是一個對象,而a是一個引用。利用指派語句,引用a指向對象1。

    為了探索對象在記憶體的存儲,我們可以利用Python内置函數id(),來檢視對象的記憶體位址。 

a = 1
b = 1

print(id(a))
print(id(b))

#  4305308800
#  4305308800
      

  可以看出 a 和 b 實際上是指向同一個對象的兩個引用。

  為了檢驗兩個引用指向同一個對象,我們可以用is關鍵字來判斷引用所指的對象是否相同

# True
a = 1
b = 1
print(a is b)

# False
a = "good"
b = "good"
print(a is b)

# False
a = []
b = []
print(a is b)
      

  從上面代碼可以看到,由于Python緩存了整數和短字元串,是以每個對象隻有一份,增加的不過是引用,而不是對象。但是,list,dict對象可以有多個相同的對象,每次指派都是建立新的對象。

  我們每次建立對象都會配置設定記憶體,容器對象跟數字不一樣呢?這是因為Python的記憶體池機制。

  Python記憶體池

      如果頻繁的調用 malloc 與 free 時,是會産生性能問題的.再加上頻繁的配置設定與釋放小塊的記憶體會産生記憶體碎片.  

Python 在這裡主要幹的工作有:

  如果請求配置設定的記憶體在1~256位元組之間就使用自己的記憶體管理系統,否則直接使用 malloc.

  這裡還是會調用 malloc 配置設定記憶體,但每次會配置設定一塊大小為256k的大塊記憶體.

引用計數

在Python中,每個對象都存有指向該對象的引用總數,即引用計數(reference count)。

Python的垃圾回收機制就是以引用計數為主,當一個對象的引用計數為0時,代表它是垃圾将要被回收。

這是引用計數增加的情況,

from sys import getrefcount      
a = [1, 2, 3]
# 對象被建立
# 對象被當成參數傳給函數
print(getrefcount(a))

b = a
# 另外的引用被建立
print(getrefcount(a))

c = [a, a]
# 作為容器對象的一個元素
print(getrefcount(a))      

  由于a和b當成了參數傳給getrefcount(),是以結果傳回2,3,5。      

這是引用計數減少的情況,

from sys import getrefcount      
a = [1, 2, 3]
b = a
c = [1, a]
print(getrefcount(a))
# 一個本地引用離開它作用域 eg:getrefcount()結束時

# del b
# 對象的别名被顯示銷毀
# print(getrefcount(a))

# b = 1
# 對象的别名被重新指派其他對象
# print(getrefcount(a))

# b.remove(a)
# 對象從容器中删除
# print(getrefcount(a))

# del c
# 對象所在容器被删除
# print(getrefcount(a))        

現在我們知道了對象引用計數的情況,那麼我們再來看一個情況。

a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
      

  a和b互相引用,導緻a和b的引用計數不可能為0,導緻記憶體暴露,是以python注定會再引入新的垃圾回收機制。

  為了解決這種孤立的引用環,Python引入了一個有效引用計數的概念,引入了标記清除法。

标記清除

『标記清除(Mark—Sweep)』算法是一種基于追蹤回收(tracing GC)技術實作的垃圾回收算法。它分為兩個階段:第一階段是标記階段,GC會把所有的『活動對象』打上标記,第二階段是把那些沒有标記的對象『非活動對象』進行回收。那麼GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?

對象之間通過引用(指針)連在一起,構成一個有向圖,對象構成這個有向圖的節點,而引用關系構成這個有向圖的邊。從根對象(root object)出發,沿着有向邊周遊對象,可達的(reachable)對象标記為活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。

Python垃圾回收機制

在上圖中,我們把小黑圈視為全局變量,也就是把它作為root object,從小黑圈出發,對象1可直達,那麼它将被标記,對象2、3可間接到達也會被标記,而4和5不可達,那麼1、2、3就是活動對象,4和5是非活動對象會被GC回收。

标記清除算法作為Python的輔助垃圾收集技術主要處理的是一些容器對象,比如list、dict、tuple,instance等,因為對于字元串、數值對象是不可能造成循環引用問題。Python使用一個雙向連結清單将這些容器對象組織起來。不過,這種簡單粗暴的标記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆記憶體,哪怕隻剩下小部分活動對象也要掃描所有對象。

分代回收

  分代回收是一種以空間換時間的操作方式,Python将記憶體根據對象的存活時間劃分為不同的集合,每個集合稱為一個代,Python将記憶體分為了3“代”,分别為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個連結清單,它們的垃圾收集頻率與對象的存活時間的增大而減小。新建立的對象都會配置設定在年輕代,年輕代連結清單的總數達到上限時,Python垃圾收集機制就會被觸發,把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活于整個系統的生命周期内。同時,分代回收是建立在标記清除技術基礎之上。分代回收同樣作為Python的輔助垃圾收集技術處理那些容器對象

a = 1
      
a = 1
b = 1

print(id(a))
print(id(b))

#  4305308800
#  4305308800
      
# True
a = 1
b = 1
print(a is b)

# False
a = "good"
b = "good"
print(a is b)

# False
a = []
b = []
print(a is b)
      

from sys import getrefcount      
a = [1, 2, 3]
# 對象被建立
# 對象被當成參數傳給函數
print(getrefcount(a))

b = a
# 另外的引用被建立
print(getrefcount(a))

c = [a, a]
# 作為容器對象的一個元素
print(getrefcount(a))      
from sys import getrefcount      
a = [1, 2, 3]
b = a
c = [1, a]
print(getrefcount(a))
# 一個本地引用離開它作用域 eg:getrefcount()結束時

# del b
# 對象的别名被顯示銷毀
# print(getrefcount(a))

# b = 1
# 對象的别名被重新指派其他對象
# print(getrefcount(a))

# b.remove(a)
# 對象從容器中删除
# print(getrefcount(a))

# del c
# 對象所在容器被删除
# print(getrefcount(a))        
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
      

Python垃圾回收機制