【python測試開發棧】—python內存管理機制(二)—垃圾回收

在上一篇文章中(python 內存管理機制—引用計數)中,咱們介紹了python內存管理機制中的引用計數,python正是經過它來有效的管理內存。今天來介紹python的垃圾回收,其主要策略是引用計數爲主,標記-清除分代回收爲輔助的策略(熟悉java的同窗回回憶下,其實這和JVM的策略是有相似之處的)。java

引用計數垃圾回收

咱們還接着上一篇文章來接着介紹引用計數的相關場景,方便咱們來理解python如何經過引用計數來進行垃圾回收。其實經過字面意思,咱們應該也不難理解,當一個對象的引用計數變爲0時,表示沒有對象再使用這個對象,至關於這個對象變成了無用的"垃圾",當python解釋器掃描到這個對象時就能夠將其回收掉。python

咱們經過一些例子來看下,可使python對象的引用計數增長或減小的場景:面試

# coding=utf-8
"""
~~~~~~~~~~~~~~~~~
 @Author:xuanke
 @contact: 784876810@qq.com
 @date: 2019-11-29 19:52
 @function: 驗證引用計數增長和減小的場景
"""
import sys

def ref_method(str):
    print(sys.getrefcount(str))
    print("我調用了{}".format(str))
    print('方法執行完了')

def ref_count():
    # 引用計數增長的場景
    print('測試引用計數增長')
    a = 'ABC'
    print(sys.getrefcount(a))
    b = a
    print(sys.getrefcount(a))
    ref_method(a)
    print(sys.getrefcount(a))
    c = [1, a, 'abc']
    print(sys.getrefcount(a))

    # 引用計數減小的場景
    print('測試引用計數減小')
    del b
    print(sys.getrefcount(a))
    c.remove(a)
    print(sys.getrefcount(a))
    del c
    print(sys.getrefcount(a))
    a = 783
    print(sys.getrefcount(a))

if __name__ == '__main__':
    ref_count()

運行結果以下:算法

測試引用計數增長
7
8
10
我調用了ABC
方法執行完了
8
9
測試引用計數減小
8
7
7
4

從上面的結果咱們得出如下結論:bash

引用計數增長的場景:

  • 對象被建立並賦值給某個變量,好比: a = 'ABC'
  • 變量間的相互引用(至關於變量指向了同一個對象),好比:b=a
  • 變量做爲參數傳到函數中。好比:ref_method(a),其實上一篇文章,咱們也提過調用getrefcount會使引用計數增長。
  • 將對象放到某個容器對象中(列表、元組、字典)。好比:c = [1, a, 'abc']

引用計數減小的場景:

  • 當一個變量離開了做用域,好比:函數執行完成時,上面的運行結果中,不知道你們發現沒,執行方法先後的引用計數保持不變,這就是由於方法執行完後,對象的引用計數也會減小,若是在方法內打印,則能看到引用計數增長的效果。
  • 對象的引用變量被銷燬時,好比del a 或者 del b。注意若是del a,再去獲取a的引用計數會直接報錯。
  • 對象被從容器對象中移除,好比:c.remove(a)
  • 直接將整個容器銷燬,好比: del c
  • 對象的引用被賦值給其餘對象,至關於變量不指向以前的對象,而是指向了一個新的對象,這種狀況,引用計數確定會發生改變。(排除兩個對象默認引用計一致的場景)。

引用計數雖然能夠實時的知道某個對象是否能夠被回收,可是也有兩個缺點:函數

  • 須要額外的空間維護引用計數。
  • 遇到有循環引用的對象,沒法有效處理。所謂循環引用就是好比:對象A引用了對象B,而對象B又引用了對象A,形成它們兩個引用計數都不能減小到0 ,所以不能被回收。

標記-回收垃圾回收

爲了解決引用計數法沒法解決的循環引用問題,python採用了標記-回收垃圾回收算法,它的整個過程分爲兩步:性能

  • 標記: 遍歷全部的對象,若是是可達的(reachable),也就是還有對象正引用它,那麼就標記該對象爲可達;
  • 清除: 再次遍歷全部的對象,若是某個對象沒有被標記爲可達,則將其回收掉。

須要注意的是在python中能夠產生循環引用問題的多是:列表、字典、用戶自定義類的對象、元組等對象,而對於數字字符串這種簡單的數據類型,並不會產生循環引用,所以後者並不在標記清除算法的考慮之列。測試

針對標記-回收垃圾回收的過程,我從網上找了幾張圖片,方便你們來了解整個過程:優化

1.png

第一張圖是初始狀態,圖片上不只有ref_count,還有一個gc_ref的值,這個gc_ref其實就是爲了來解決引用計數問題的,它是ref_count的一個副本,因此它的初始值和ref_count保持一致。當開始遍歷全部對象時,當發現link1引用了link2對象時,會將link2的gc_ref值減小1,如此類推,就獲得下圖的結果。操作系統

2.png

第二張圖中咱們看到link二、link三、link4的gc_ref都已經爲0,當python垃圾回收器再次掃描全部對象時,那麼它們就會被標記爲GC_TENTATIVELY_UNREACHABLE,同時被移到Unreachable列表中。有同窗可能會疑惑爲啥link2沒有被移到Unreachable列表中,其實它理論上也應該被移到Unreachable列表中,如第三張圖所示:

3.png

若是python垃圾回收器再次掃描對象時,發現某個對象的ref_count不爲0,那麼就會將其標記爲GC_REACHABLE,表示還正在被引用着,以下圖所示的link1就是這種狀況。

4.png

除了將link1標記爲可達的以外,python垃圾回收器,還會從當前可達節點依次遍歷全部可達的節點,好比從link1能夠到達link2和link3,但link3已經被放到Unreachable列表中,所以還須要將link3再移回到Object to Scan列表中,表示對象仍是能夠觸達的。最終的結果以下圖所示,只有link4會被回收掉:

5.png

標記-清除法雖然能夠解決循環引用的問題,可是缺點也比較明顯,就是須要python垃圾回收器對python對象執行兩遍掃描,而每次掃描,python解釋器就會暫停處理其餘事情,等到掃描結束後才能恢復正常。這個過程就比如:圖書管理員要對圖書館進行清潔整理,那麼將會關閉圖書館,直到收拾乾淨後才能從新打開圖書館,供同窗們使用。

分代垃圾回收

那既然在python垃圾回收過程當中,會暫停整個應用程序,有沒有更好的優化方案呢?答案是確定的。在python解釋器中,對象的存活時間是不同的:

  • 長時間存活(或一直存活)的對象,它們是內存垃圾的可能性低,能夠減小對它們掃描的次數。
  • 臨時或短期存活的對象,這種對象比較容易成爲內存垃圾,因此得頻繁掃描。
  • 位於前兩種狀況的之間的對象。可根據狀況進行內存掃描。

這樣區分對象後,就能夠節省每次掃描的時間(不須要全部對象都掃描),重而能提高垃圾回收的速度。

python中結合着上面列出的三種類型的對象分了三個對象代(0,1,2),它們其實對應了3個鏈表:每個新生對象在generation zero中,若是它在一輪gc掃描中活了下來,那麼它將被移至generation one,在這一個對象代掃描次數將會減小;若是它又活過了一輪gc,它又將被移至generation two,在這一個對象代對象掃描次數將會更少。

python觸發垃圾回收掃碼的時機

python解釋器只會在觸發某個條件時,纔會去執行垃圾回收。這個條件就是當python分配對象的次數和取消分配對象的次數(引用計數變爲0)作差值高於某個閾值,咱們能夠經過python提供的方法來查看這個閾值。

def threshold_gc():
    # 獲取閾值
    print(gc.get_threshold())
    # 可設置閾值
    gc.set_threshold(800, 10, 10)
    print(gc.get_threshold())

# 運行結果
(700, 10, 10)  
(800, 10, 10)

上面程序運行結果中值的含義以下:

  • 700是垃圾回收啓動的閾值。
  • 後面兩個10與分代回收有關(上面介紹過python分了三個對象代:0、一、2),第一個10表示每進行10次0代對象掃描,則進行1次1代對象掃描。
  • 最後一個10表示每進行10次1代對象掃描,則執行1次2代對象掃描。

此外能夠本身根據狀況,調用set_threshold()方法來調整垃圾回收的頻率。好比:set_threshold(700,10,5),至關於增長了對2代對象的掃描頻率。

gc這個庫中還有一些很好玩的函數,你們能夠了解下(更多方法能夠參考官方文檔):

def gc_method():
    # 啓動垃圾回收
    gc.enable()
    # 停用垃圾回收
    gc.disable()
    # 手動指定垃圾回收,參數能夠指定垃圾回收的代數,不填寫參數就是徹底的垃圾回收
    gc.collect()
    # 設置垃圾回收的標誌,多用於內存泄漏的檢測
    gc.set_debug(gc.DEBUG_LEAK)
    # 返回一個對象的引用列表
    gc.get_referrers()

額外補充-python內存分層結構

在python中,內存管理機制被抽象成分層次的結構,從python解釋器Cpython的源碼obmallic.c的註釋中抓取了對內存分層的描述:

/*
    Object-specific allocators
    _____   ______   ______       ________
   [ int ] [ dict ] [ list ] ... [ string ]       Python core         |
+3 | <----- Object-specific memory -----> | <-- Non-object memory --> |
    _______________________________       |                           |
   [   Python's object allocator   ]      |                           |
+2 | ####### Object memory ####### | <------ Internal buffers ------> |
    ______________________________________________________________    |
   [          Python's raw memory allocator (PyMem_ API)          ]   |
+1 | <----- Python memory (under PyMem manager's control) ------> |   |
    __________________________________________________________________
   [    Underlying general-purpose allocator (ex: C library malloc)   ]
 0 | <------ Virtual memory allocated for the python process -------> |
   =========================================================================
    _______________________________________________________________________
   [                OS-specific Virtual Memory Manager (VMM)               ]
-1 | <--- Kernel dynamic storage allocation & management (page-based) ---> |
    __________________________________   __________________________________
   [                                  ] [                                  ]
-2 | <-- Physical memory: ROM/RAM --> | | <-- Secondary storage (swap) --> |

*/
  • 第-2層是物理內存層。
  • 第-1層是操做系統虛擬的內存管理器。
  • 第0層是C中的malloc、free等內存分配和釋放相關的層。當申請的內存大於256K時,會調用第0層的malloc分配內存。
  • 第1層和第2層是python級別的內存分配器(內存池),當申請的內存小於256K時,會由這兩層來進行處理。這兩層存在3個級別的內存結構:arena>pool>block,其中arena大小固定是256K,pool的固定大小是4K,而block的大小是8的整數倍,用來知足最小分配需求。
  • 第3層是python對象內存分配器,也就是咱們一般所用的python對象,好比:列表和字典、元組等。

python的內存這麼分層設計,最根本的目的仍是爲了提升python的執行性能,由於若是不分層,頻繁的調用malloc和free,很是的耗費系統資源,會產生性能問題。而分層以後,第1層和第2層充當了內存池的做用,根據分配的內存大小不一樣,交給不一樣的層去處理,減小了頻繁的調用malloc。

總結

本文介紹了python中垃圾回收的三種方式,以及python內存的分層管理方式,屬於比較深層次的python知識,不過相信也能夠幫助你瞭解python的內存管理方式。若是在以後找工做過程當中再被面試官問道"python垃圾回收機制"這樣的問題,假如你能將文中的內容講出來絕對是加分項。

相關文章
相關標籤/搜索