淺析Python垃圾回收機制


概述

程序是指在執行的過程中動態的申請內存空間,隨着程序的運行不再需要使用這些內存空間。這時如果不釋放這些空間,就會駐留內存成為無用的垃圾,也就是造成了內存泄漏。
垃圾回收機制:GC,垃圾回收機制的存在,使得開發人員可以把更多的精力關注業務邏輯,而不是內存中垃圾的回收,因此GC的存在幫助了程序開發人員管理內存。
Python中的垃圾回收以引用計數為主,標記清除和分代回收為輔,同時還有緩存機制。

一、引用計數

1、環狀雙向鏈表refchain

在Python程序執行時,會創建一個環狀雙向鏈表refchain, 程序執行過程中創建的任何一個對象最終都會加入到這個雙向鏈表中。
比如:

name = 'weilanhanf 
age = 100 
hobby = ["eat", "sleep"]

以上三個不同類型的對象,首先對進行初始化,然后進行一下封裝成雙向鏈表節點,根據數據類型,會封裝一些不同的屬性。然后插入到雙向鏈表中

封裝的屬性包括:[上一個對象的引用,下一個對象的引用,對象類型,引用個數]
name = 'weilanhanf'
new_name = name  # 引用個數屬性+1

封裝的屬性包括:[上一個對象的引用,下一個對象的引用,對象類型,引用個數, value=100]
age = 100

封裝的屬性包括:[上一個對象的引用,下一個對象的引用,對象類型,引用個數, items, 元素個數等]
hobby = ["eat", "sleep"]

在Python中,一切都是對象。在Cpython中,每個Py對象又都對應個一個C語言結構體,其都有相同的屬性:PyObject(包含4個屬性:前后引用,引用計數器,數據類型)。如果是容器類型,還會有額外的ob_size(元素個數)等屬性。

2、類型結構體

比如浮點類型

data = 3.14
CPython內部對象創建,包含屬性:
_obj_next = rechain中上一個對象
_obj_pre = rechain中下一個對象
ob_refcat = 1
ob_type = float
ob_fval = 3.14

其他類型對象也類似,都對應着一個相似的結構體對象

3、引用計數器

如程序中有如下代碼

v1 = 3.14
v2 = 999
v3 = (1, 2, 3 )

python解釋器初始化的時候,就會生成refchain鏈表。執行Python程序的時候,底層會逐個創建refchain鏈表節點對象,對象會根據程序中變量類型初始化屬性,然后加入到refchain鏈表中。節點對象中維護一個refcnct的值,也就是引用計數器,記錄對該對象的引用個數。當有其他對象增加引用,引用計數器的值+1。當減少引用,值-1。

v1 = 3.14

# 增加引用
v4 = v1  

# 減少引用
del v4
del v1

當有一個對象的引用計數器值為0,意味着對象無用,這個對象在內存中認為是垃圾,需要進行銷毀。從rechain鏈表中移除,然后進行緩存或者回收其所占用的內存,詳細見如下的緩存機制。

4、循環引用問題

引用計數通過記錄對象是否被引用。但是可能存在容器對象中的元素分別又是別的容器對象,這樣就會產生循環引用問題。如下:

v1 = [11,22,33]  # refchain中創建一個列表對象。由於v1=對象,所以列表引對象用計數器為1.
v2 = [44,55,66]  # 把v2追加到v1中,則v2對應的[44,55,66]對象的引用計數器加1,最終為2.

v1.append(v2)  # 把v2追加到v1中,則v2對應的[44,55,66]對象的引用計數器加1,最終為2.
v2.append(v1)  # 把v2追加到v1中,則v2對應的[44,55,66]對象的引用計數器加1,最終為2.

del v1  # 引用計數器-1
del v2  # 引用計數器-1

del操作之后,v1,v2的引用都為1。雖然刪除引用了,默認無用。但是兩個數數組還在鏈表中,常駐內存,成為垃圾占用內存。
所以python引用標記清除解決這個循環引用存在的問題。

二、標記清除

目的:為了解決循環引用產生的問題。
實現:在Python的底層再維護一個鏈表,專門存放可能存在循環引用的對象(列表,字典,元組等)。
也就是除了refchain雙向循環鏈表之外還要在維護一個鏈表,暫且稱為鏈表A。當創建的容器對象,還要再添加到第二個鏈表A中。

在執行的過程中,某些情況下,會去掃描循環引用的鏈表A中的每個元素,檢查是否有循環引用。如果有,讓循環引用的雙方的引用計數器-1,如果引用計數器為0,則認為是垃圾,從鏈表移除,進行回收。
使用標記清除也會有兩個問題:

  • 什么時候掃描存放容器類型對象的鏈表A?
  • 掃描鏈表然后在檢測是否有循環引用本身會很耗時,怎么解決?

三、分代回收

為了解決標記清除中的兩個問題,將存放可能存在循環引用的對象的鏈表A分成了3個鏈表:

  • 0代鏈表:0代中對象達到700個,掃描0代鏈表
  • 1代鏈表:0代鏈表掃描10次,則1代鏈表掃描1次
  • 2代鏈表:1代鏈表掃描10次,則2代鏈表掃描1次

所有的容器對象先添加的時候,都要先放到0代鏈表中,然后依次往1代,2代中添加。當0代鏈表達到700,進行掃描0代鏈表。如果鏈表存在循環引用的對象,其引用計數器-1。如果計數器為0,進行垃圾回收。如果不為0,則放入到1代鏈表中,此次1代鏈表記錄0代鏈表掃描次數+1。
同時解決了標記回收的兩個問題:
什么時候掃描:當鏈表達到閾值時候進行掃描
掃描耗時問題:分成三條鏈表,減少耗時

四、Python的GC小結

綜上,Python中的垃圾回收以引用計數為主,標記清除和分代回收為輔。
程序執行的過程中維護了一個refchain的雙向環狀鏈表,這個鏈表中存儲程序創建的所有對象,每種類型的對象中都有一個ob_ refcnt引用計數器的值會根據引用個數動態+1、-1。最后當引用計數器變為0時會進行垃圾回收(對象銷毀、refchain中移除)。
但是,那些可以有多個元素組成的對象可能會存在循環引用的問題,為了解決這個問題Python又引入了標記清除和分代回收。執行過程中,維護了4個鏈表,

refchain
2代鏈表
1代鏈表
0代鏈表

分代鏈表當達到各自的閾值時,就會觸發掃描鏈表進行標記清除的動作。

五、緩存機制

Python中,不同類型的對象有自己的緩存機制。

1、 小數據池

有時候在創建同一個對象,可能會出現,其地址相同的現象。比如:

v1 = 7
v2 = 9
v3 = 9

此時為同一個對象,變量指向內存地址一致
id(v2) == id(v3)  # True

這就是因為開辟了小對象池的原因,v2和v3指向的是同一塊內存空間。
為了避免重復創建和銷毀一些常見對象,Python會維護一個對象池,或者說是小數據池,其中包括一些int和短字符對象。比如啟動解釋器時候,Python創建的整型小數據池包括:-5,-4,... 257,不包括257和-5,開區間。
小數據對象池中對象中的引用計數器在創建時添加引用默認為1,所以無論程序中自己編寫的變量添加引用或者減少引用,小數據池中對象的引用計數器都不會為0,也都不會被回收,程序執行過程中永遠駐留內存。

2、free_list緩存

free_list緩存適用於float/list/tuple/dict等類型。
當一個以上類型的對象的引用計數器為0的時候,其實不會從refchain鏈表剔除然后立即回收,而是添加到一個free_list鏈表中當作緩存,以備后續使用。如果有新的對象被創建,不用再重新開辟內存,而是從free_list中取緩存。

v1 = 3.14  # 開辟內存,創建對象,初始化引用計數器等值,加入到refchain中

del v1  # 從refcain中移除,然后將節點對象添加到free_list中

v9 = 999.99  # 不會立即開辟空間創建對象,如果free_list不為空,則是從free_list中獲取緩存,然后初始化從緩存中獲取的對象,然后在添加到refchain中

同樣free_list也是有上限的,如果free_list滿,比如閾值為80,則從refchain中移除的對象不會緩存到free_list中,而是立即銷毀。

六、Golang的GC小結

Golang中的GC隨着版本的提高,GC機制也越來越高效。

  • V1.3 普通標記清除法,整體過程需要STW(Stop The World),效率低下
  • V1.5 三色標記法,堆空間啟用寫屏障,棧空間不啟動,全部掃描之后,還需要一次STW掃描棧,效率一般
  • V1.8 三色標記法,讀和寫屏障機制,棧空間不啟動,堆空間啟動,整體過程幾乎不需要STW,效率較高
    V1.8表示1.8版本,對比Golang的也就是,通常的GC都要將程序中對象用鏈表或者其他的數據結構組織起來,然后根據對象之間的引用鏈,逐個掃描對象,標記對象是否為內存垃圾然后決定是否回收。

:以上僅為自己的學習筆記,如有相同或者不對地方,輕噴,謝謝謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM