👏作者簡介:大家好,我是愛敲代碼的小王,CSDN博客博主,Python小白
📕系列專欄:python入門到實戰、Python爬蟲開發、Python辦公自動化、Python數據分析、Python前后端開發
📧如果文章知識點有錯誤的地方,請指正!和大家一起學習,一起進步👀
🔥如果感覺博主的文章還不錯的話,請👍三連支持👍一下博主哦
🍂博主正在努力完成2023計劃中:以夢為馬,揚帆起航,2023追夢人
🔥🔥🔥 python入門到實戰專欄:從入門到實戰?
🔥🔥🔥 Python爬蟲開發專欄:從入門到實戰
🔥🔥🔥?Python辦公自動化專欄:從入門到實戰
🔥🔥🔥 Python數據分析專欄:從入門到實戰
🔥🔥🔥 Python前后端開發專欄:從入門到實戰
目錄
內存管理機制
Python緩存機制
垃圾回收機制
分代回收機制
內存管理機制
Python是由C語言開發的,底層操作都是基于C語言實現,Python中創建每個對象,內部都會與C語言結構體維護一些值。?
源碼下載,https://www.python.org/
將壓縮文件減壓,可以看到有很多文件,主要關心兩個(Include、 Objects) 在Include目錄下object.h中可以查看創建對象的結構體。?
#define _PyObject_HEAD_EXTRA \struct _object *_ob_next; \struct _object *_ob_prev;
typedef struct _object {_PyObject_HEAD_EXTRAPy_ssize_t ob_refcnt;PyTypeObject *ob_type;
} PyObject;
typedef struct {PyObject ob_base;Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
在創建對象時,每個對象至少內部4個值,PyObject結構體(上一個 對象、下一個對象、類型、引用個數)。
有多個元素組成的對象使用PyVarObject,里面由:PyObject結構體(上一個對象、下一個對象、類型、引用個數)+Ob_size(items=元素,元素個數)。
環狀雙向鏈表refchain
在python程序中創建的任何對象都會被放在refchain鏈表中。
類型封裝結構體
f = 3.14
'''
內部會創建:
1.開辟內存
2.初始化ob_fval = 3.14 值ob_type = float 類型ob_refcnt = 1 引用數量
3.將對象加入到雙向鏈表refchain中_ob_next = refchain中的上一個對象_ob_prev = refchain中的下一個對象
'''
Python緩存機制
小整數對象池?
一些整數在程序中的使用非常廣泛,Python為了優化速度,使用了小整數對象池, 避免為整數頻繁申請和銷毀內存空間。
Python 對小整數的定義是 [-5, 257) 這些整數對象是提前建立好的,不會被垃圾回收。在一個 Python 的程序中,所有位于這個范圍內的整數使用的都是同一個對象。在一個 Python 的程序中,無論這個整數處于LEGB(局部變量,閉包,全局,內建模塊)中的哪個位置, 所有位于這個范圍內的整數使用的都是同一個對象。
【示例】驗證小整數池
In [8]: a = 100In [9]: id(a)
Out[9]: 140705185112832#刪除變量a
In [10]: del aIn [11]: b = 100In [12]: id(b)
Out[12]: 140705185112832
大整數對象池
每一個大整數,均創建一個新的對象。
【示例】驗證大整數池
In [13]: a = 257In [14]: b = 257In [15]: id(a)
Out[15]: 1747036560496In [16]: id(b)
Out[16]: 1747036558352In [17]: a is b
Out[17]: False
intern機制
每個單詞(字符串),不夾雜空格或者其他符號,默認開啟intern機制,共享內存,靠引用計數決定是否銷毀。
【示例】intern機制
>>> a = 'helloworld'
>>> b = 'helloworld'
>>> a is b
>>> a = 'hello world'
>>> b = 'hello world'
>>> a is b
free_list機制
當一個對象的引用計數器為0時,按理說應該回收,但內存不會直接回收,而是將對象添加到free_list鏈表中緩存。以后再去創建對象 時,不再重新開辟內存,而是直接使用free_list。
以上的free_list的代表:float,list,tuple,dict。
>>> f1 = 3.14
>>> id(f1)
2078136427568
>>> del f1
>>> f2 = 9.998
>>> id(f2)
2078136427568
f1 = 3.14,會創建float類型,并且加入refchain中。del f1 則減1, refchain移除,按理會進行銷毀,但是實際中不會真正銷毀,而是 會添加到free_list。以后創建f2=9.998等,只要是float類型,則不會重新開辟內存,會去free_list獲取對象,對象內部進行初始化, 替換值3.14換成9.99,再放到refchain中。不是所有的都放入 free_list。例如存儲100個緩存,則前面新創建的80個會放入 free_list。如果滿了81之后則會銷毀。
1、 float類型,維護的free_list鏈表最多可緩存100個float對象。
#ifndef PyFloat_MAXFREELIST//定義free_list的最大長度
#define PyFloat_MAXFREELIST 100
#endif
2、 list類型,維護的free_list數組最多可緩存80個list對象。
#ifndef PyList_MAXFREELIST#define
PyList_MAXFREELIST 80
#endif
【示例】
>>> v1 = [11,22,33]
>>> id(v1)
2078137539904
>>> del v1
>>> v2 = ['a','b']
>>> id(v2)
2078137539904
3、 tuple類型,維護一個free_list數組且數組含量20,數組中元素 可以是鏈表且每個鏈表最多可以含鈉2000個元組對象。元組的 free_list數據在存儲數據時,是根據元組可容納的個數為索引找 到free_list數組中對應的鏈表,并添加到鏈表中。
>>> t1 = (1,2,3)
>>> id(t1)
2078137564992
>>> del t1 #元組的數量是3,所以把這個對象緩存到
free_list[3]的鏈表中
>>> t2 = ('a','b','c')
#元組的數量也是3,不會重新開辟內存,而是去
free_list[3]對應的鏈表中拿到一個對象來使用
>>> id(t2)
2078137564992
>>> del t2
>>> t3 = (11,22,33,44)
>>> id(t3)
2078138219472
?4、 dict類型,維護的free_list數組最多可緩存80個dict對象。
#ifndef PyDict_MAXFREELIST#define
PyDict_MAXFREELIST 80
#endif
【示例】
>>> d1 = {'name':'zs'}
>>> id(d1)
2078137472640
>>> del d1
>>> d2 = {'age':30}
>>> id(d2)
2078137472640
垃圾回收機制
Python 內部采用 引用計數法 ,為每個對象維護引用次數,并據此回收不再需要的垃圾對象。由于引用計數法存在重大缺陷,循環引 用時有內存泄露風險,因此 Python 還采用 標記清除法 來回收存在循環引用的垃圾對象。此外,為了提高垃圾回收( GC )效率,Python 還引入了 分代回收機制 。?
?引用計數法
Python采用了類似Windows內核對象一樣的方式來對內存進行管理。每一個對象,都維護這一個對指向該對對象的引用的計數。
引用計數 是計算機編程語言中的一種 內存管理技術 ,它將資源被引用的次數保存起來,當引用次數變為 0 時就將資源釋放。它管理 的資源并不局限于內存,還可以是對象、磁盤空間等等。
Python 也使用引用計數這種方式來管理內存,每個 Python 對象都包含一個公共頭部,頭部中的 ob_refcnt 字段便用于維護對象被引用 次數。回憶對象模型部分內容,我們知道一個典型的 Python 對象結構如下:
當創建一個對象實例時,先在堆上為對象申請內存,對象的引用計數被初始化為 1 。以 Python 為例,我們創建一個 float 對象保存 6.66,并把它賦值到變量 f :?
>>> f = 6.66
>>> f
6.66
由于此時只有變量 f 引用 float 對象,因此它的引用計數為 1 :
當我們把 pi 賦值給 f 后,float 對象的引用計數就變成了 2 ,因為現在有兩個變量引用它:?
>>> ff = f
>>> ff
6.66
我們新建一個 list 對象,并把 float 對象保存在里面。這樣一來, float 對象有多了一個來自 list 對象的引用,因此它的引用計數又加 一,變成 3 了:?
>>> l = [f]
>>> l
[6.66]
標準庫 sys 模塊中有一個函數 getrefcount 可以獲取對象引用計數:
>>> import sys
>>> sys.getrefcount(pi)
4
?咦!引用計數不應該是 3 嗎?為什么會是 4 呢?由于 float 對象被作為參數傳給 getrefcount 函數,它在函數執行過程中作為函數的局部變量存在,創建了一個臨時的引用,因此又多了一個引用:
隨著 getrefcount 函數執行完畢并返回,它的棧幀對象將從調用鏈中解開并銷毀,這時 float 對象的引用計數也跟著下降。因此,當一個 對象作為參數傳個函數后,它的引用計數將加一;當函數返回,局部名字空間銷毀后,對象引用計數又減一。
引用計數就這樣隨著引用關系的變動,不斷變化著。當所有引用都消除后,引用計數就降為零,這時 Python 就可以安全地銷毀對象, 回收內存了:?
>>> del l
>>> del ff
>>> del f
引用計數增加
1、 對象被創建
2 、如果有新的對象使用該對象
3 、作為容器對象的一個元素
4、 被作為參數傳遞給函數
引用計數減少
1、 對象的引用被顯示的銷毀
2、 新對象不再使用該對象
3 、對象從列表中被移除,或者列表對象本身被銷毀
4 、函數調用結束?
引用計數機制的優點?
1、簡單
2、實時性:一旦沒有引用,內存就直接釋放了。
?引用計數機制的缺點
1、維護引用計數消耗資源
2、循環引用的問題無法解決
a = [1,2]
b = [3,4]
a.append(b) #b的計數器2
b.append(a) #a的計數器2
del a
del b
?標記-清除
引用計數法能夠解決大多數垃圾回收的問題,但是遇到兩個對象相互引用的情況,del語句可以減少引用次數,但是引用計數不會歸 0,對象也就不會被銷毀,從而造成了內存泄漏問題。針對該情況, Python引入了標記-清除機制。
循環引用?
引用計數這種管理內存的方式雖然很簡單,但是有一個比較大的瑕疵,即它不能很好的解決循環引用問題。如上圖所示:對象 A 和對 象 B,相互引用了對方作為自己的成員變量,只有當自己銷毀時, 才會將成員變量的引用計數減 1。因為對象 A 的銷毀依賴于對象 B 銷毀,而對象 B 的銷毀與依賴于對象 A 的銷毀,這樣就造成了我們 稱之為循環引用(Reference Cycle)的問題,這兩個對象即使在外界已經沒有任何指針能夠訪問到它們了,它們也無法被釋放。?
class People:pass
class Cat:pass
#創建People的實例對象
p = People()
#創建Dog的實例對象
c = Cat()
#People的寵物屬性指向Cat
p.pet = c
#Cat的主人屬性指向People
c.master = p
#刪除p和c對象
del p
del c
上述實例中,對象p中的屬性引用c,而對象c中屬性同時來引用p, 從而造成僅僅刪除p和c對象,也無法釋放其內存空間,因為他們依然在被引用。深入解釋就是,循環引用后,p和c被引用個數為2, 刪除p和c對象后,兩者被引用個數變為1,并不是0,而python只有 在檢查到一個對象的被引用個數為0時,才會自動釋放其內存,所以 這里無法釋放p和c的內存空間。
主動思路一般分為兩步:垃圾識別 和 垃圾回收 。垃圾對象被識別出來后,回收就只是自然而然的工作了,因此垃圾識別是解決問題的關鍵。那么,有什么辦法可以將垃圾對象識別出來呢?我們來考察一個一般化例子:
這是一個對象引用關系圖,其中灰色部分是需要回收但由于循環引用而無法回收的垃圾對象,綠色部分是被程序引用而不能回收的活躍對象。如果我們能夠將活躍對象逐個遍歷并標記,那么最后沒有被標記的對象就是垃圾對象。
遍歷活躍對象,第一步需要找出 根對象 ( root object )集合。所謂根對象,就是指被全局引用或者在棧中引用的對象,這部對象是不能被刪除的。因此,我們將這部分對象標記為綠色,作為活躍對象遍歷的起點。?
根對象本身是 可達的 ( reachable ),不能刪除;被根對象引用的對象也是可達的,同樣不能刪除;以此類推。我們從一個根對象出發,沿著引用關系遍歷,遍歷到的所有對象都是可達的,不能刪除。?
?這樣一來,當我們遍歷完所有根對象,活躍對象也就全部找出來了:
而沒有被標色的對象就是 不可達 ( unreachable )的垃圾對象,可以被安全回收。循環引用的致命缺陷完美解決了!?
這就是垃圾回收中常用的 標記清除法 。?
【示例】標記清除法
?上圖中小黑點(變量)表示根節點,從根節點出發,每個對象都有引用和被引用的情況,如果該對象找不到根節點,那么就會被清除,如圖1,2,3都有被小黑點(變量)引用,4,5沒有變量引用,所以 4,5就會被清除。
分代回收機制
Python 程序啟動后,內部可能會創建大量對象。如果每次執行標記清除法時,都需要遍歷所有對象,多半會影響程序性能。為此, Python引入分代回收機制——將對象分為若干“代”( generation ), 每次只處理某個代中的對象,因此 GC 卡頓時間更短。
考察對象的生命周期,可以發現一個顯著特征:一個對象存活的時間越長,它下一刻被釋放的概率就越低。我們應該也有這樣的親身體會:經常在程序中創建一些臨時對象,用完即刻釋放;而定義為 全局變量的對象則極少釋放。
因此,根據對象存活時間,對它們進行劃分就是一個不錯的選擇。 對象存活時間越長,它們被釋放的概率越低,可以適當降低回收頻率;相反,對象存活時間越短,它們被釋放的概率越高,可以適當提高回收頻率。
對象存活時間 | 釋放概率 | 回收頻率 |
長 | 低 | 低 |
短 | 高 | 高 |
Python 內部根據對象存活時間,將對象分為 3 代(見 Include/internal/mem.h ):
#define NUM_GENERATIONS 3
?隨著時間的推進,程序冗余對象逐漸增多,達到一定閾值,系統進行回收。
這 3 個代分別稱為:初生代、中生代 以及 老生代。當這 3 個代初始化完畢后,對應的 gc_generation 數組大概是這樣的:
import gc
#python 中內置模塊gc觸發
print(gc.get_threshold()) #查看gc默認值
#輸出(700, 10, 10)
第一代鏈表:
當第一代達到700,就開始檢測哪些對象引用計數變成0了,把不是0的放到第二代鏈表里,此時第一代鏈表就是空了,當再次達到700 時,就再檢測一遍。
第二代鏈表:
當第二代鏈表達到10,就檢測一次。
第三代鏈表:
第三代鏈表檢測10之后,第三代鏈表檢測一次。
import gc#返回一個元組,分別獲取這三代當前計數
gc.get_count() #返回一個元組,分別獲取這三代當前的收集閾值
gc.get_threshold()#設置閾值
gc.set_threshold()#關閉gc垃圾回收機制
gc.disable()