溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python中垃圾回收機制的原理分析

發布時間:2022-05-09 17:22:31 來源:億速云 閱讀:260 作者:iii 欄目:大數據

這篇“Python中垃圾回收機制的原理分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Python中垃圾回收機制的原理分析”文章吧。

垃圾回收機制

「垃圾回收(GC)」 大家應該多多少少都了解過,什么是垃圾回收呢?垃圾回收GC的全拼是 Garbage Collection,在維基百科的定義是:在計算機科學中,垃圾回收(英語:Garbage Collection,縮寫為GC)是一種自動的內存管理機制。當一個電腦上的動態內存不再需要時,就應該予以釋放,以讓出內存,這種內存資源管理,稱為垃圾回收。我們都知道在C/C++里用戶需要自己管理維護內存,自己管理內存是很自由,可以隨意申請、釋放內存,但是極易會出現內存泄露,懸空指針等問題;像現在的高級語言Java,Python等,都采用了垃圾回收機制,自動進行內存管理,而垃圾回收機制專注于兩件事:「① 找到內存中無用的垃圾資源?!?/strong> 「② 清除這些垃圾資源并把內存讓出來給其他對象使用?!?/strong>

Python作為一門解釋型語言,因為簡單易懂的語法,我們可以直接對變量賦值,而不必聲明變量的類型,變量類型的確定、內存空間的分配與釋放都是由Python解釋器在運行時自動進行的,我們不必關心;Python這一自動管理內存的功能極大的減少了開發者的編碼負擔,讓開發者專注于業務實現,這也是成就Python自身的重要原因之一。接下來,我們就扒一扒python的內存管理。

引用計數機制

Python中一切皆對象,也就是說,在Python中你用到的一切變量,本質上都是類對象。實際上每一個對象的核心就是一個「結構體PyObject」,它的內部有一個引用計數器ob_refcnt,程序在運行的過程中會實時的更新ob_refcnt的值,來反映引用當前對象的名稱數量。當某對象的引用計數值為0,說明這個對象變成了垃圾,那么它會被回收掉,它所用的內存也會被立即釋放掉。

typedef struct _object {
    int ob_refcnt;//引用計數
    struct _typeobject *ob_type;
} PyObject;

以下情況是導致引用計數加一的情況:
對象被創建,例如a=5
對象被引用,b=a
對象被作為參數,傳入到一個函數中(要注意的是,在函數調用發生的時候,會產生額外的兩次引用,一次來自函數棧,另一個是函數參數)
對象作為一個元素,存儲在容器中(例如存儲在列表中)

下面的情況則會導致引用計數減一:
對象別名被顯示銷毀 del a
對象別名被賦予新的對象
一個對象離開它的作用域
對象所在的容器被銷毀或者是從容器中刪除對象

我們還可以通過sys包中的getrefcount()來獲取一個名稱所引用的對象當前的引用計數(注意,這里getrefcount()本身會使得引用計數加一)

import sys
a = [1, 2, 3]
print(sys.getrefcount(a))# 輸出為2,說明有兩次引用(一次來自a的定義,一次來自getrefcount)def func(a):    print(sys.getrefcount(a))
    # 輸出為4,說明有四次引用(a的定義、Python的函數調用棧,函數參數,和getrefcount)func(a)
print(sys.getrefcount(a))# 輸出為2,說明有兩次引用(一次來自a的定義,一次來自getrefcount),此時函數func調用已經不存在

下面從使用內存的角度看一下:

import osimport psutildef show_memory_info(hint):    """
    顯示當前 python 程序占用的內存大小
    :param hint:
    :return:
    """    pid = os.getpid()
    p = psutil.Process(pid)

    info = p.memory_full_info()
    memory = info.rss / 1024 / 1024    print('{} 當前進程的內存使用: {} MB'.format(hint, memory))def func():    show_memory_info('初始')
    a = [i for i in range(9999999)]
    show_memory_info('創建a之后')


func()
show_memory_info('結束')

輸出如下:

初始 當前進程的內存使用: 12.125 MB
創建a之后 當前進程的內存使用: 205.15625 MB
結束 當前進程的內存使用: 12.87890625 MB

可以看出,當前進程初始的內存使用為12.125 MB,當調用了函數func()創建列表a之后,內存占用迅速增加到了205.15625 MB,而在函數調用結束后,內存則返回正常。這是因為,函數內部聲明的列表a是局部變量,在函數返回后,局部變量的引用會注銷掉,此時列表a所指代對象的引用計數為0,Python 便會執行垃圾回收,因此之前占用的大量內存就又回來了。

循環引用

何為循環引用?簡單來說就是兩個對象相互引用??聪旅嬉欢纬绦颍?/p>

def func2():
    show_memory_info('初始')
    a = [i for i in range(10000000)]
    b = [x for x in range(10000001, 20000000)]
    a.append(b)
    b.append(a)
    show_memory_info('創建a,b之后')

func2()
show_memory_info('結束')

輸出如下:

初始 當前進程的內存使用: 12.14453125 MB
創建a,b之后 當前進程的內存使用: 396.6875 MB
結束 當前進程的內存使用: 396.96875 MB

可以看出,在程序中,a和b互相引用,并且作為局部變量在函數func2調用結束后,a和b從程序意義上已經不存在,但從輸出結果中看到,依然有內存占用,這是為什么呢?因為互相引用導致它們的引用數都不為0。

如果在生產環境下出現了循環引用,又沒有其他垃圾回收機制的情況下,經過長時間運行后,程序所占用的內存一定會變得越來越大,如果沒有被及時處理,一定會跑滿服務器的。

如果不得不使用循環引用的話,我們可以顯式調用「gc.collect()」 來啟動垃圾回收:

def func2():
    show_memory_info('初始')
    a = [i for i in range(10000000)]
    b = [x for x in range(10000001, 20000000)]
    a.append(b)
    b.append(a)
    show_memory_info('創建a,b之后')

func2()
gc.collect()
show_memory_info('結束')

輸出如下:

初始 當前進程的內存使用: 12.29296875 MB
創建a,b之后 當前進程的內存使用: 396.69140625 MB
結束 當前進程的內存使用: 12.95703125 MB

引用計數機制有高效、簡單、實時性(一旦為零就直接做掉)等優點,一旦一個對象的引用計數歸零,內存就直接釋放了。不用像其他機制等到特定時機。將垃圾回收隨機分配到運行的階段,處理回收內存的時間分攤到了平時,正常程序的運行比較平穩。但是,引用計數也存在著一些缺點,通常的缺點有:

① 邏輯雖然簡單,但維護起來有些麻煩。每個對象需要分配單獨的空間來統計引用計數,并且需要對引用計數進行維護,這是需要消耗一下資源的。
② 循環引用。這將是引用計數機制的致命傷,引用計數對此是無解的,因此必須要使用其它的垃圾回收算法對其進行補充。

事實上,Python 使用標記清除(mark-sweep)算法和分代收集(generational),來啟用針對循環引用的自動垃圾回收。

標記清除解除循環引用

Python采用了 「標記-清除(Mark and Sweep)算法」,解決容器對象可能產生的循環引用問題。(注意,只有容器類對象才有可能產生循環引用,比如列表、字典、用戶自定義類的對象、元組等。而像數字,字符串這類簡單類型不會出現循環引用。作為一種優化策略,對于只包含簡單類型的元組也不在標記清除算法的考慮之列)
它分為兩個階段:第一階段是標記階段,GC會把所有的「活動對象」打上標記,第二階段是把那些沒有標記的「非活動對象」進行回收。
那么Python又是如何判斷什么樣的對象為非活動對象的呢?
對于任何對象集合,我們先建個引用計數副本表,來存它們的引用計數,然后把集合內部的引用都解除掉(內部引用是指這個集合中的某個對象引用了本集合內部的另一個對象),解除的過程中在副本表減少引用計數,解除掉所有的內部引用后,在副本表引用計數依然不為0的,就是根集合,然后開始標記過程,即從跟集合節點逐步恢復引用并增加副本表的引用計數,最后副本表中引用計數為0的,就是垃圾對象了,我們就需要對它們進行垃圾回收。例如:

Python中垃圾回收機制的原理分析

上面這個集合中的節點有外部進來的連接(到a和到b),也有到外部的連接(c引用了外面某個對象),右邊是引用計數表,然后我們拆掉所有內部連接: 

Python中垃圾回收機制的原理分析

那么根集合就是a和b了,然后我們從a和b出發開始標記并恢復引用計數: 

Python中垃圾回收機制的原理分析

從a和b出發可達的節點都被恢復了,引用計數還是0的就是這個集合內部循環引用的垃圾(e和f),如果把所有對象看做一個集合,那么可以回收所有垃圾,也可以將所有對象劃分成一個個小的集合,分別回收小集合內的垃圾。
但是每次都需要遍歷圖,對于Python而言是一種巨大的性能浪費。

分代回收

分代回收是一種以空間換時間的操作方式,Python將內存根據對象的存活時間劃分為不同的集合,每個集合稱為一個代,Python將內存分為了3代,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代)。它們對應3個鏈表,它們的垃圾收集頻率隨對象的存活時間的增大而減小。
新創建的對象都會分配在年輕代,年輕代鏈表的總數達到上限時,即當垃圾回收器中新增對象減去刪除對象達到相應的閾值時,就會對這一代對象啟動垃圾回收,把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活于整個系統的生命周期內。同時,分代回收是建立在標記清除技術基礎之上。事實上,分代回收基于的思想是,新生的對象更有可能被垃圾回收,而存活更久的對象也有更高的概率繼續存活。因此,通過這種做法,可以節約不少計算量,從而提高Python的性能。

以上就是關于“Python中垃圾回收機制的原理分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女