首頁 > 軟體

Python萬字深入記憶體管理講解

2022-07-21 18:02:27

Python記憶體管理

一、物件池

1.小整數池

系統預設建立好的,等著你使用

概述:

整數在程式中的使用非常廣泛,Python為了優化速度,使用了小整數物件池,避免為整數頻繁申請和銷燬記憶體空間。

Python 對小整數的定義是 [-5, 256] ,這些整數物件是提前建立好的,不會被垃圾回收。

在一個 Python 的程式中,無論這個整數處於LEGB(區域性變數,閉包,全域性,內建模組)中的哪個位置,所有位於這個範圍內的整數使用的都是同一個物件。

# 互動式環境下:
>>> a = 100
>>> b = 100
>>> id(a)
140720433537792
>>> id(b)
140720433537792
>>> a is b
True
>>> 

我們可以看出a,b指向同一個記憶體地址。

2.大整數池

大整數池:預設建立出來,池內為空的,建立一個就會往池中儲存一個

# python互動式環境
>>> a = 257
>>> b = 257
>>> id(a)
2085029722896
>>> id(b)
2085029722960
>>> a is b
False
>>> 

a , b 不是指向同一個記憶體地址。

python中對大於256的整數,會重新分配物件空間地址儲存物件。

3.inter機制(短字串池)

每個單詞(字串),不夾雜空格或者其他符號,預設開啟intern機制,共用記憶體,靠參照計數決定是否銷燬。

>>> s1 = 'hello'
>>> s2 = 'hello'
>>> id(s1)
2178093449264
>>> id(s2)
2178093449264
>>> s1 is s2
True
>>> 

字串s1和s2中沒有空格時,可以看出,這裡的s1與s2指向同一個記憶體地址。

當我們在he和llo之間加一個空格

>>> s1 = 'he llo'
>>> s2 = 'he llo'
>>> id(s1)
2278732636592
>>> id(s2)
2278732636528
>>> s1 is s2
False
>>> 

這時的字串s1和s2就沒有指向同一個記憶體地址。

對於字串來說,如果不包含空格的字串,則不會重新分配物件空間,對於包含空格的字串則會重新分配物件空間。

二、垃圾回收

概述:

python採用的是參照計數機制為主,隔代回收和標記清除機制為輔的策略

概述:
現在的高階語言如java,c# 等,都採用了垃圾收集機制,而不再是c,c++裡使用者自己管理維護記憶體的方式。

自己管理 記憶體極其自由, 可以任意申請記憶體,但如同一把雙刃劍,為大量記憶體洩露,懸空指標等bug埋下隱患。 

python裡也同java一樣採用了垃圾收集機制,不過不一樣的是: 
python採用的是參照計數機制為主,隔代回收機制為輔的策略

2.1.參照計數

在Python中,每個物件都有指向該物件的參照總數---參照計數 

檢視物件的參照計數:sys.getrefcount() 

注意:
    當使用某個參照作為引數,傳遞給getrefcount()時,引數實際上建立了一個臨時的參照。
    因此, getrefcount()所得到的結果,會比期望的多1。

2.1.1 參照計數增加

a、物件被建立

b、另外變數也指向當前物件

c、作為容器物件的一個元素

d、作為引數提供給函數:test(x)

2.1.2 參照計數減少

a、變數被顯式的銷燬

b、物件的另外一個變數重新賦值

c、從容器中移除

d、函數被執行完畢

看程式碼:

# -*- coding: utf-8 -*-
import sys
class Test(object):
    def __init__(self):
        print('當前物件已經被建立,佔用的記憶體地址為:%s' % hex(id(self)))
a = Test()
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 2
b = a
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 3
list1 = []
list1.append(a)
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 4
del b
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 3
list1.remove(a)
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 2
del a
print('當前物件的參照計數為:%s' % sys.getrefcount(a))  # 報錯
'''
Traceback (most recent call last):
  File "E:/Python Project/Python 高階程式設計/記憶體管理/垃圾收集.py", line 30, in <module>
    print('當前物件的參照計數為:%s' % sys.getrefcount(a))
NameError: name 'a' is not defined
'''

當Python的某個物件的參照計數降為0時,說明沒有任何參照指向該物件,該物件就成為要被回收的垃圾。比如某個新建物件,被分配給某個參照,物件的參照計數變為1。如 為0,那麼該物件就可以被垃圾回收。

2.2.標記清除

標記清除(Mark—Sweep)演演算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收演演算法。

它分為兩個階段:

第一階段是標記階段,GC會把所有的活動物件打上標記

第二階段是把那些沒有標記的物件非活動物件進行回收。

物件之間通過參照(指標)連在一起,構成一個有向圖,物件構成這個有向圖的節點,而參照關係構成這個有向圖的邊。從根物件(root object)出發,沿著有向邊遍歷物件,可達的(reachable)物件標記為活動物件,不可達的物件就是要被清除的非活動物件。根物件就是全域性變數、呼叫棧、暫存器。

>

在上圖中,可以從程式變數直接存取塊1,並且可以間接存取塊2和3。程式無法存取塊4和5。第一步將標記塊1,並記住塊2和3以供稍後處理。第二步將標記塊2,第三步將標記塊3,但不記得塊2,因為它已被標記。掃描階段將忽略塊1,2和3,因為它們已被標記,但會回收塊4和5。

標記清除演演算法作為Python的輔助垃圾收集技術,主要處理的是一些容器物件,比如list、dict、tuple等,因為對於字串、數值物件是不可能造成迴圈參照問題。Python使用一個雙向連結串列將這些容器物件組織起來。不過,這種簡單粗暴的標記清除演演算法也有明顯的缺點:清除非活動的物件前它必須順序掃描整個堆記憶體,哪怕只剩下小部分活動物件也要掃描所有物件。

2.3.分代回收

因為, 標記和清除的過程效率不高。清除非活動的物件前它必須順序掃描整個堆記憶體,哪怕只剩下小部分活動物件也要掃描所有物件。還有一個問題就是:什麼時候掃描去檢測迴圈參照?

為了解決上述的問題,python又引入了分代回收。分代回收解決了標記清楚時什麼時候掃描的問題,並且將掃描的物件分成了3級,以及降低掃描的工作量,提高效率。

  • 0代: 0代中物件個數達到700個,掃描一次。
  • 1代: 0代掃描10次,則1代掃描1次。
  • 2代: 1代掃描10次,則2代掃描1次。

隔代回收是用來解決交叉參照(迴圈參照),並增加資料回收的效率. 原理:通過物件存在的時間不同,採用不同的演演算法來 回收垃圾.

形象的比喻, 三個連結串列,零代連結串列上的物件(新建立的物件都加入到零代連結串列),參照數都是一,每增加一個指標,參照加一,隨後 python會檢測列表中的互相參照的物件,根據規則減掉其參照計數.

GC演演算法對連結串列一的參照減一,參照為0的,清除,不為0的到連結串列二,連結串列二也執行GC演演算法,連結串列三一樣. 存在時間越長的 資料,越是有用的資料

2.3.1 分代回收觸發時機?(GC閾值)

隨著你的程式執行,Python直譯器保持對新建立的物件,以及因為參照計數為零而被釋放掉的物件的追蹤。

從理論上說,這兩個值應該保持一致,因為程式新建的每個物件都應該最終被釋放掉。當然,事實並非如此。因為迴圈 參照的原因,從而被分配物件的計數值與被釋放物件的計數值之間的差異在逐漸增長。一旦這個差異累計超過某個閾 值,則Python的收集機制就啟動了,並且觸發上邊所說到的零代演演算法,釋放“浮動的垃圾”,並且將剩下的物件移動到 一代列表。

隨著時間的推移,程式所使用的物件逐漸從零代列表移動到一代列表。而Python對於一代列表中物件的處理遵循同樣的 方法,一旦被分配計數值 與被釋放計數值累計到達一定閾值,Python會將剩下的活躍物件移動到二代列表。

通過這種方法,你的程式碼所長期使用 的物件,那些你的程式碼持續存取的活躍物件,會從零代連結串列轉移到一代再轉移到二代。

通過不同的閾值設定,Python可 以在不同的時間間隔處理這些物件。

Python處理零代最為頻繁,其次是一代然後才是二代。

2.3.2 檢視參照計數(gc模組的使用)

# 引入gc模組
import gc 
# 常用函數: 
gc.get_count() 
# 獲取當前自動執行垃圾回收的計數器,返回一個長度為3的列表
gc.get_threshold() 
# 獲取gc模組中自動執行垃圾回收的頻率 
gc.set_threshold(threshold0[,threshold1,threshold2]) 
# 設定自動執行垃圾回收的頻率 
gc.disable() 
# python3預設開啟gc機制,可以使用該方法手動關閉gc機制 
gc.collect() 
# 手動呼叫垃圾回收機制回收垃圾

記憶體管理是使用計算機必不可少的一部分,無論好壞,Python幾乎會在後臺處理一切記憶體管理的問題。Python抽象出許多使用計算機的嚴格細節,這讓我們在更高層次進行開發,而不用擔心所有位元組的儲存方式和位置。

# -*- coding: utf-8 -*-
import gc
import sys
import time
class Test(object):
    def __init__(self):
        print('當前物件已經被建立,佔用的記憶體地址為:%s' % hex(id(self)))
    def __del__(self):
        print('當前物件馬上被系統GC回收')
# gc.disable()  # 不啟用GC,在python3中預設啟用
while True:
    a = Test()
    b = Test()
    a.pro = b  # a 和 b之間相互參照
    b.pro = a
    del a
    del b
    print(gc.get_threshold())  # 列印隔代回收的閾值
    print(gc.get_count())  # 列印GC需要回收的物件
    time.sleep(0.2)  # 休眠0.2秒方便檢視

終端輸出:

三、怎麼優化記憶體管理

1.手動垃圾回收

先呼叫del a ; 再呼叫gc.collect()即可手動啟動GC

2.調高垃圾回收閾值

gc.set_threshold 設定垃圾回收閾值(收集頻率)。

將 threshold 設為零會禁用回收。

3.避免迴圈參照

四、總結

python採用的是參照計數機制為主,標記-清除和分代回收(隔代回收)兩種機制為輔的策略

到此這篇關於Python萬字深入記憶體管理講解的文章就介紹到這了,更多相關Python記憶體管理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com