1. 程式人生 > >【python測試開發棧】—python記憶體管理機制(二)—垃圾回收

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

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

引用計數垃圾回收

我們還接著上一篇文章來接著介紹引用計數的相關場景,方便我們來理解python如何通過引用計數來進行垃圾回收。其實通過字面意思,我們應該也不難理解,當一個物件的引用計數變為0時,表示沒有物件再使用這個物件,相當於這個物件變成了無用的"垃圾",當python直譯器掃描到這個物件時就可以將其回收掉。

我們通過一些例子來看下,可以使python物件的引用計數增加或減少的場景:

# coding=utf-8
"""
~~~~~~~~~~~~~~~~~
 @Author:xuanke
 @contact: [email protected]
 @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

從上面的結果我們得出以下結論:

引用計數增加的場景:

  • 物件被建立並賦值給某個變數,比如: 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中可以產生迴圈引用問題的可能是:列表、字典、使用者自定義類的物件、元組等物件,而對於數字字串這種簡單的資料型別,並不會產生迴圈引用,因此後者並不在標記清除演算法的考慮之列。

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

第一張圖是初始狀態,圖片上不僅有ref_count,還有一個gc_ref的值,這個gc_ref其實就是為了來解決引用計數問題的,它是ref_count的一個副本,所以它的初始值和ref_count保持一致。當開始遍歷所有物件時,當發現link1引用了link2物件時,會將link2的gc_ref值減少1,如此類推,就得到下圖的結果。

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

如果python垃圾回收器再次掃描物件時,發現某個物件的ref_count不為0,那麼就會將其標記為GC_REACHABLE,表示還正在被引用著,如下圖所示的link1就是這種情況。

除了將link1標記為可達的之外,python垃圾回收器,還會從當前可達節點依次遍歷所有可達的節點,比如從link1可以到達link2和link3,但link3已經被放到Unreachable列表中,因此還需要將link3再移回到Object to Scan列表中,表示物件還是可以觸達的。最終的結果如下圖所示,只有link4會被回收掉:

標記-清除法雖然可以解決迴圈引用的問題,但是缺點也比較明顯,就是需要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、1、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垃圾回收機制"這樣的問題,假如你能將文中的內容講出來絕對是加分項。

相關推薦

python測試開發python記憶體管理機制垃圾回收

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

你必須瞭解的java記憶體管理機制-垃圾回收

本文在個人技術部落格不同步釋出,詳情可用力戳 亦可掃描螢幕右側二維碼關注個人公眾號,公眾號內有個人聯絡方式,等你來撩... 相關連結(注:文章講解JVM以Hotspot虛擬機器為例,jdk版本為1.8) 1、 你必須瞭解的java記憶體管理機制-執行時資料區 2、 你必須瞭解的java記憶體管理機制-記

python測試開發python記憶體管理機制—引用計數

什麼是記憶體 在開始進入正題之前,我們先來回憶下,計算機基礎原理的知識,為什麼需要記憶體。我們都知道計算機的CPU相當於人類的大腦,其運算速度非常的快,而我們平時寫的資料,比如:文件、程式碼等都是儲存在磁碟上的。磁碟的存取速度完全不能匹配cpu的運算速度,因此就需要一箇中間層來適配兩者的不對等,記憶體由此而來

python測試開發python基礎語法大盤點

周邊很多同學在用python,但是偶爾會發現有人對python的基礎語法還不是特別瞭解,所以幫大家梳理了python的基礎語法(文中的介紹以python3為例)。如果你已然是python大牛,可以跳過這篇文章。 編碼 python3的預設編碼格式是:UTF-8 ,換句話說也就是:如果不在檔案頭部宣告編碼格式,

iOS開發中的ARC記憶體管理機制1——基礎概念

由於移動裝置的記憶體資源一般比較少,所以垃圾回收機制的操作會對裝置的效能造成比較明顯的影響,有可能在執行垃圾回收的時候讓移動裝置出現卡頓,這對於使用者來說是很難受的事。 由此蘋果公司提出了ARC方案。 0x01 自動引用計數 自動引用計數(Automatic Ref

深入理解java虛擬機器之自動記憶體管理機制

垃圾收集演算法     java中的記憶體是交給虛擬機器管理的。要實現垃圾回收,必須考慮如下三個問題:     1. 哪些記憶體需要回收?     2. 什麼時候回收?     3. 怎麼回收?     對於第一點,往大了來說,是堆和方法區的記憶體需要回收。往具體了來說,是堆中哪些物件的記憶體可以回

輕量級作業系統FreeRTOS的記憶體管理機制

本文由嵌入式企鵝圈原創團隊成員朱衡德(Hunter_Zhu)供稿。FreeRTOS多種記憶體管理機制中最簡單的一種:全域性宣告一個靜態陣列ucHeap,然後通過指標偏移記錄空間的分配情況,在這種記憶體機

你必須瞭解的java記憶體管理機制-垃圾標記

本文在個人技術部落格不同步釋出,詳情可用力戳 亦可掃描螢幕右側二維碼關注個人公眾號,公眾號內有個人聯絡方式,等你來撩... 相關連結(注:文章講解JVM以Hotspot虛擬機器為例,jdk版本為1.8) 1、 你必須瞭解的java記憶體管理機制-執行時資料區 2、 你必須瞭解的java記憶體管理機制-記

python測試開發帶你徹底搞明白python3編碼原理

在之前的文章中,我們介紹過編碼格式的發展史:[文章傳送門-todo]。今天我們通過幾個例子,來徹底搞清楚python3中的編碼格式原理,這樣你之後寫python指令碼時碰到編碼問題,才能有章可循。 我們先搞清楚幾個概念: 系統預設編碼:指python直譯器預設的編碼格式,在python檔案頭部沒有宣告其他編

python測試開發—理解python深拷貝與淺拷貝的區別

記憶體的淺拷貝和深拷貝是面試時經常被問到的問題,如果不能理解其本質原理,有可能會答非所問,給面試官留下不好的印象。另外,理解淺拷貝和深拷貝的原理,還可以幫助我們理解Python記憶體機制。這篇文章將會通過一些例子,來驗證記憶體拷貝的過程,幫助大家理解記憶體拷貝的原理。 Python3中的資料型別 我們首先得知

python測試開發—幫你總結Python os模組高頻使用的方法

Python中的os模組是主要和系統操作相關的模組,在平時的工作中會經常用到,花時間整理了os模組的高頻使用方法,同時整理出使用時需要注意的點。歸納來講,os模組的方法可以分為:目錄操作、檔案操作、路徑操作、系統操作等四大類,我們接下來依次進行介紹。 目錄操作相關 建立、刪除、重新命名目錄 # 當前目

python深入之python記憶體管理機制重點

關於python的儲存問題 (1)由於python中萬物皆物件,所以python的儲存問題是物件的儲存問題,並且對於每個物件,python會分配一塊記憶體空間去儲存它 (2)對於整數和短小的字元等,python會執行快取機制,即將這些物件進行快取,不會為相同的物件分配多個

java虛擬機器記憶體管理機制:JVM記憶體管理總結分享

近期看了看Java記憶體洩露的一些案例,跟原來的幾個哥們討論了一下,深入研究發現JVM裡面還是有不少以前不知道的細節,這裡稍微剖析一下。先看一看JVM的內部結構——如圖所示,JVM主要包括兩個子系統和兩個元件。兩個子系統分別是Class loader子系統和Execution

安全牛學習筆記SQLMAP自動註入

信息安全 security+ SQLMAP自動註入(二)-REQUEST和SQLMAP自動註入(三)-OPTIMIZATIONSQLMAP自動註入02-----REQUEST--delay 每次http(s)請求之間延遲時間,浮點數,單位為秒,默認無延遲--timeout 請求超時時間,

自動記憶體管理機制5- 虛擬機器效能監控

自動記憶體管理機制(5)- 虛擬機器效能監控 0. 概述 在我們日常開發的專案中,有時經常會碰到以下問題: OOM(OutOfMemoryError),記憶體不足 記憶體洩漏 執行緒死鎖 Lock Contention,鎖爭用 Java程序消耗CP

自動記憶體管理機制4- 記憶體分配和回收策略

自動記憶體管理機制(4)- 記憶體分配和回收策略 Java所承諾的自動記憶體管理主要是針對物件記憶體的回收和物件記憶體的分配。 在Java虛擬機器的五塊記憶體空間中,程式計數器、Java虛擬機器棧、本地方法棧記憶體的分配和回收都具有確定性,一般在編譯階段就能確定需要分配的記憶體大小,

自動記憶體管理機制3-HotSpot垃圾收集器

自動記憶體管理機制(3)-HotSpot垃圾收集器 如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。 這裡討論的收集器都是JDK1.7(包含JDK1.7)以後的HotSpot虛擬機器: 上半部屬於新生代收集器,下半部屬於老年代收集器。如果兩個收集器

自動記憶體管理機制2- 記憶體回收垃圾收集演算法

自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法 1. 概述 首先思考三個問題: 哪些記憶體需要回收 什麼時候回收 如何回收 程式計數器、虛擬機器棧、本地方法棧是執行緒私有的,因此這幾個區域的記憶體分配和回收都具有確定性(執行緒結束時執行垃圾回

自動記憶體管理機制1- java記憶體區域與虛擬機器物件

自動記憶體管理機制(1)- java記憶體區域與虛擬機器物件 1. 執行時資料區域 Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。有的區域隨著虛擬機器進行的啟動而存在,有些區域則以來使用者執行緒的啟動和結束而建立和銷燬。 有以下幾個區域

深入理解java虛擬機器之自動記憶體管理機制

  各類垃圾收集器與GC日誌 (一)垃圾收集器   一、Serial收集器     最基本、歷史最悠久的收集器。使用複製演算法,用在新生代,通常老年代用Serial old配合。GC過程需要stop the world。適用於client模式下的虛擬機器。   二、ParNew收集器