1. 程式人生 > >【淺析】Python的記憶體管理機制

【淺析】Python的記憶體管理機制

python的記憶體管理分為三個方面:

  • 引用計數
  • 垃圾回收
  • 記憶體池機制

淺析引用計數

python內部使用引用計數,來保持追蹤記憶體中的物件,Python內部記錄了物件有多少個引用,即引用計數,當物件被建立時就建立了一個引用計數,當物件不再需要時,這個物件的引用計數為0時,它被垃圾回收。

引用計數增加的情況:

1.物件被建立:x=4
2.另外的別人被建立:y=x
3.被作為引數傳遞給函式:foo(x)
4.作為容器物件的一個元素:a=[1,x,’33’]

引用計數減少情況

1.一個本地引用離開了它的作用域。比如上面的foo(x)函式結束時,x指向的物件引用減1。
2.物件的別名被顯式的銷燬:del x ;或者del y
3.物件的一個別名被賦值給其他物件:x=789
4.物件從一個視窗物件中移除:myList.remove(x)
5.視窗物件本身被銷燬:del myList,或者視窗物件本身離開了作用域。

如何獲取一個變數的引用計數


>> import sys
>> x = 1
>> sys.getrefcount(x)
599
>> y = x
>> sys.getrefcount(x)
600
>> del y
>> sys.getrefcount(x)
599

垃圾回收

python的垃圾回收機制以引用計數為主,標記-清除和分代收集為輔。

  1. 引用計數
    優點:“實時性”,任何記憶體,一旦沒有指向它的引用,就會立即被回收。
    缺點:
    (1). 效率底下:引用計數機制帶來的計數操作,與引用賦值成正比。頻繁的技術操作,會給CPU帶來大量消耗。
    (2). 迴圈引用:也就是兩個物件相互引用,這樣的話,兩個物件的引用計數永遠不會為0,及它們永遠不被清除。

  2. 標記-清除
    標記-清除是為了解決迴圈引用的問題。可以包含其他物件引用的容器物件(比如:list,set,dict,class,instance)都可能產生迴圈引用。
    2.1 假設
    如果兩個物件的引用計數都為1的話,但僅僅存在它們之間的相互引用,那麼,我們可以認為這兩個物件的實際引用計數為0.如果我們將這個引用迴圈去掉,那麼它們的實際引用計數才會顯現。
    案例:有迴圈引用的A,B兩個物件,從A出發,因為它有一個對B的引用,則將B的引用計數減1;然後順著引用達到B,因為B有一個對A的引用,同樣將A的引用減1,這樣,就完成了迴圈引用物件間環摘除。
    問題:如果A,B間沒有迴圈引用,但A引用了B,B沒有以用A,貿然的將B計數引用減1,而A沒有被回收,這將導致在未來的某個時刻出現一個對B的懸空引用,類似與C的空指標異常。這就要求我們必須在A沒有被刪除的情況下復原B的引用計數,那麼維護引用計數的複雜度將成倍增加。
    2.2 標記-清除的原理
    原理:
    我們並不改動真實的引用計數,而是將集合中物件的引用計數複製一份副本,改動該物件引用的副本。對於副本做任何的改動,都不會影響到物件生命走起的維護。
    這個計數副本的唯一作用是尋找root object集合(該集合中的物件是不能被回收的)。當成功尋找到root object集合之後,首先將現在的記憶體連結串列一分為二,一條連結串列中維護root object集合,成為root連結串列,而另外一條連結串列中維護剩下的物件,成為unreachable連結串列。之所以要剖成兩個連結串列,是基於這樣的一種考慮:現在的unreachable可能存在被root連結串列中的物件,直接或間接引用的物件,這些物件是不能被回收的,一旦在標記的過程中,發現這樣的物件,就將其從unreachable連結串列中移到root連結串列中;當完成標記後,unreachable連結串列中剩下的所有物件就是名副其實的垃圾物件了,接下來的垃圾回收只需限制在unreachable連結串列中即可。
    效率分析:
    從垃圾收集機制來看,這種垃圾收集機制所帶來的額外操作實際上與系統中總的記憶體塊的數量是相關的,當需要回收的記憶體塊越多時,垃圾檢測帶來的額外操作就越多,而垃圾回收帶來的額外操作就越少;反之,當需回收的記憶體塊越少時,垃圾檢測就將比垃圾回收帶來更少的額外操作。

  3. 分代回收
    3.1 理論:
    無論使用何種語言開發,無論開發的是何種型別,何種規模的程式,都存在這樣一點相同之處。即:一定比例的記憶體塊的生存週期都比較短,通常是幾百萬條機器指令的時間,而剩下的記憶體塊,起生存週期比較長,甚至會從程式開始一直持續到程式結束。
    3.2 原理:
    將系統中的所有記憶體塊根據其存活時間劃分為不同的集合,每一個集合就成為一個“代”,垃圾收集的頻率隨著“代”的存活時間的增大而減小。也就是說,活得越長的物件,就越不可能是垃圾,就應該減少對它的垃圾收集頻率。那麼如何來衡量這個存活時間:通常是利用幾次垃圾收集動作來衡量,如果一個物件經過的垃圾收集次數越多,可以得出:該物件存活時間就越長。也就是符合馬太福音,存活久的讓它繼續存活下去。

記憶體池機制

Python的記憶體機制以金字塔層次:

  1. 記憶體分配層次:

      -1,-2層主要有作業系統進行操作,   第0層是C中的malloc,free等記憶體分配和釋放函式進行操作;
      第1層和第2層是記憶體池,有Python的介面函式PyMem_Malloc函式實現,當物件小於256K時有該層直接分配記憶體;    第3層是最上層,也就是我們對Python物件的直接操作;

  2. 原因
    • 在 C 中如果頻繁的呼叫 malloc 與 free 時,是會產生效能問題的.再加上頻繁的分配與釋放小塊的記憶體會產生記憶體碎片。
  3. 具現化
    Python提供了對記憶體的垃圾收集機制,但是它將不用的記憶體放到記憶體池而不是返回給作業系統。
      Python中所有小於256個位元組的物件都使用pymalloc實現的分配器,而大的物件則使用系統的 malloc。另外Python物件,如整數,浮點數和List,都有其獨立的私有記憶體池,物件間不共享他們的記憶體池。也就是說如果你分配又釋放了大量的整數,用於快取這些整數的記憶體就不能再分配給浮點數。
      在Python中,許多時候申請的記憶體都是小塊的記憶體,這些小塊記憶體在申請後,很快又會被釋放,由於這些記憶體的申請並不是為了建立物件,所以並沒有物件一級的記憶體池機制。這就意味著Python在執行期間會大量地執行malloc和free的操作,頻繁地在使用者態和核心態之間進行切換,這將嚴重影響 Python的執行效率。為了加速Python的執行效率,Python引入了一個記憶體池機制,用於管理對小塊記憶體的申請和釋放。這也就是之前提到的 Pymalloc機制。
      Pymalloc 關於釋放記憶體方面,當一個物件的 引用計數變為0時,python就會呼叫它的解構函式。在析構時,也採用了記憶體池機制,從記憶體池來的記憶體會被歸還到記憶體池中,以避免頻繁地釋放動作。
      Pymalloc分配一系列256KB的記憶體塊,稱之為arena。每個arena分割為4KB大小的記憶體池Pool,每個Pool再切分為固定大小的Block。在記憶體分配時,分配給程序的就是這些Blocks。
最後打一個廣告,喜歡的這篇博文的歡迎大家轉發,收藏,關注我~謝謝~