剖析 Python 面試知識點(二)- 記憶體管理和垃圾回收機制

天澄發表於2019-04-01

剖析 Python 面試知識點(二)- 記憶體管理和垃圾回收機制

1.記憶體管理

Python 中一切皆物件,物件又可以分為可變物件和不可變物件。二者可以通過原地修改,如果修改後地址不變,則是可變物件,否則為不可變物件,地址資訊可以通過id()進行檢視。

>>> a = 10
>>> id(a)
4339392960
>>> a =11
>>> id(a)
4339392992
>>> a = [1, 2]
>>> id(a)
4342877192
>>> a.append(3)
>>> a
[1, 2, 3]
>>> id(a)
4342877192
複製程式碼

Python 有記憶體池機制,Pymalloc機制,用於對記憶體的申請和釋放管理。先來看一下為什麼有記憶體池:

當建立大量消耗小記憶體的物件時,c中頻繁呼叫new/malloc會導致大量的記憶體碎片,致使效率降低。

記憶體池的概念就是預先在記憶體中申請一定數量的,大小相等的記憶體塊留作備用,當有新的記憶體需求時,就先從記憶體池中分配記憶體給這個需求,不夠了之後再申請新的記憶體。這樣做最顯著的優勢就是能夠減少記憶體碎片,提升效率。

檢視原始碼,可以看到 Pymalloc 對於小的物件,Pymalloc會在記憶體池中申請空間,一般是少於236kb,如果是大的物件,則直接呼叫 new/malloc 來申請新的記憶體空間。

有了記憶體的建立,那就需要回收,垃圾回收機制,也是 Python 面試當中必問的一個知識點,接下來看看垃圾回收機制是什麼?

2. 垃圾回收機制

垃圾回收機制,Python 採用 GC 作為自動記憶體管理機制,GC要做的有2件事,一是找到記憶體中無用的垃圾物件資源,二是清除找到的這些垃圾物件,釋放記憶體給其他物件使用。

如何實現上述2點了,Python 採用了 引用計數 為主, 標誌清除和分代回收 為輔測策略。

2.1 引用計數

檢視原始碼,每一個物件,在原始碼裡就是一個結構體表示,都會有一個計數字段.

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;
複製程式碼

PyObject是每個物件必有的內容,其中ob_refcnt就是做為引用計數。當一個物件有新的引用時,它的ob_refcnt就會增加,當引用它的物件被刪除,它的ob_refcnt就會減少。 一旦物件的引用計數為0,該物件立即被回收,物件佔用的記憶體空間將被釋放。

此演算法的優點和缺點都是非常明顯的:

優點:

  • 簡單
  • 實時性:一旦沒有引用,記憶體就直接釋放了。不用像其他機制等到特定時機。

缺點:

  • 需要額外的空間維護引用計數。
  • 不能解決物件的迴圈引用。(主要缺點)

接下來說一下什麼是迴圈引用:

A和B相互引用而且沒有外部引用A與B中的任何一個。也就是物件之間互相應用,導致引用鍊形成一個環。

>>>>>>a = { } #物件A的引用計數為 1
>>>b = { } #物件B的引用計數為 1
>>>a['b'] = b  #B的引用計數增1
>>>b['a'] = a  #A的引用計數增1
>>>del a #A的引用減 1,最後A物件的引用為 1
>>>del b #B的引用減 1, 最後B物件的引用為 1
複製程式碼

執行 del 後,A、B物件已經沒有任何引用指向這兩個物件,但是這兩個物件各包含一個對方物件的引用,雖然最後兩個物件都無法通過其它變數來引用這兩個物件了,這對GC來說就是兩個非活動物件或者說是垃圾物件。理論上是需要被回收的。 按上面的引用計數原理,要計數為0才會回收,但是他們的引用計數並沒有減少到零。因此如果是使用引用計數法來管理這兩物件的話,他們並不會被回收,它會一直駐留在記憶體中,就會造成了記憶體洩漏(記憶體空間在使用完畢後未釋放)。

為了解決物件的迴圈引用問題,Python 引入了標記清除和分代回收兩種GC機制。

2.2 標記清除

標記清除主要是解決迴圈引用問題。

標記清除演算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收演算法。 它分為兩個階段:第一階段是標記階段,GC會把所有的 活動物件 打上標記,第二階段是把那些沒有標記的物件 非活動物件 進行回收。那麼GC又是如何判斷哪些是活動物件哪些是非活動物件的呢?

物件之間通過引用(指標)連在一起,構成一個有向圖,物件構成這個有向圖的節點,而引用關係構成這個有向圖的邊。從根物件(root object)出發,沿著有向邊遍歷物件,可達的(reachable)物件標記為活動物件,不可達的物件就是要被清除的非活動物件。根物件就是全域性變數、呼叫棧、暫存器。

剖析 Python 面試知識點(二)- 記憶體管理和垃圾回收機制

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

標記清除演算法作為 Python 的輔助垃圾收集技術主要處理的是容器物件(container,上面講迭代器有提到概念),比如list、dict、tuple等,因為對於字串、數值物件是不可能造成迴圈引用問題。Python使用一個雙向連結串列將這些容器物件組織起來。

Python 這種簡單粗暴的標記清除演算法也有明顯的缺點:清除非活動的物件前它必須順序掃描整個堆記憶體,哪怕只剩下小部分活動物件也要掃描所有物件。

2.3 分代回收

分代回收是一種以空間換時間的操作方式。

Python將記憶體根據物件的存活時間劃分為不同的集合,每個集合稱為一個代,Python將記憶體分為了3“代”,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個連結串列,它們的垃圾收集頻率與物件的存活時間的增大而減小。新建立的物件都會分配在年輕代,年輕代連結串列的總數達到上限時,Python垃圾收集機制就會被觸發,把那些可以被回收的物件回收掉,而那些不會回收的物件就會被移到中年代去,依此類推,老年代中的物件是存活時間最久的物件,甚至是存活於整個系統的生命週期內。同時,分代回收是建立在標記清除技術基礎之上。

分代回收同樣作為Python的輔助垃圾收集技術處理容器物件。

『剖析Python面試知識點』完整內容請檢視 : gitbook.cn/gitchat/act…

更多精彩文章請關注公眾號: 『天澄技術雜談』

剖析 Python 面試知識點(二)- 記憶體管理和垃圾回收機制

相關文章