Python記憶體管理知識整理

2022-05-12 20:37:19 字數 4135 閱讀 1051

當建立物件時, python 立即向作業系統請求記憶體

可以用id(變數名)來獲取該變數所引用物件的記憶體位址

>>> a=1

>>> print(id(a))

56780120

is關鍵字用於判斷引用是否相同,==用於判斷引用的內容是否相同

>>> a=

>>> b=

>>> a==b

true

>>> id(a)

44204920l

>>> id(b)

45830760l

>>> a is b

false

>>> a="123"

>>> b="123"

>>> a is b

true

>>> id(a)

45845320l

>>> id(b)

45845320l

在python中,整數和短小的字元,python都會快取這些物件,以便重複使用。當我們建立多個等於「123」的引用時,實際上是讓所有這些引用指向同乙個物件。

當某個物件被建立並賦值給變數時,該物件的引用計數都被設定為1,再次被引用會增加該物件的引用計數,而當物件的引用被銷毀,引用計數會減小。

檢視乙個物件的引用計數:

if __name__ == '__main__':

from sys import getrefcount

arr = [4,5,6,7,0,1,2]

print(getrefcount(arr))

# 2

使用某個物件的引用作為getrefcount的引數時,此引數實際上建立了乙個物件的臨時引用,在python當中,所有的傳參都是引用傳遞。因此getrefcount返回的引用計數是該物件實際的引用計數+1

getrefcount不僅僅統計當前**塊對物件的引用計數,還統計了import模組中對物件的引用計數。在python的內建模組中,可能有很多對數字1的引用,因此

>>> from sys import getrefcount

>>> getrefcount(1)

102

乙個物件的引用計數變為0後,使用者不可能通過任何方式動用這個物件,但是,此物件占用的記憶體並不會立即被**,因為python在執行垃圾**時會暫停其他所有任務,因此垃圾**只會在必要的時候執行

可以使用gc.get_count()檢視gc實時計數情況驗證這一點

>>> gc.get_count()

(560, 10, 0)

560表示距離上一次0代垃圾檢查,python分配記憶體的數目減去釋放記憶體的數目

10表示距離上次1代垃圾檢查,0代垃圾檢查的數量

0表示距離上次2代垃圾檢查,1代垃圾檢查的數量

>>> gc.get_count()

(560, 10, 0)

>>> a=[1,2,3]

>>> id(a)

23419440

>>> gc.get_count()

(561, 10, 0)

// 為物件[1,2,3]分配了記憶體

>>> del a

>>> gc.get_count()

(561, 10, 0)

// 刪除引用a後,並沒有立即**[1,2,3]占用的記憶體

>>> a=[1,2,3]

>>> gc.get_count()

(561, 10, 0)

>>> id(a)

23419440

// 沒有重新建立[1,2,3]物件,因此分配記憶體的數目沒有增加

// a再次引用了之前沒有被**的物件[1,2,3],它們的記憶體位址是一樣的

>>> a=[1,2,3,4]

>>> gc.get_count()

(562, 10, 0)

// 為物件[1,2,3,4]分配了記憶體,[1,2,3]引用計數歸零,但還是沒有**[1,2,3]占用的記憶體

引用計數法最主要的缺點在於不能解決物件的迴圈引用問題

注意:只有容器物件才會產生迴圈引用的情況,比如列表、字典、使用者自定義類的物件、元組等。而像數字,字串這類簡單型別不會出現迴圈引用。

a =  # 變數a指向物件a,a的引用計數為 1

b = # 變數b指向物件b,b的引用計數為 1

a['b'] = b # b的引用計數增1

b['a'] = a # a的引用計數增1

del a # a的引用計數減 1,最後a物件的引用為 1

del b # b的引用計數減 1, 最後b物件的引用為 1

我們已經不能通過任何變數訪問到a、b物件,但是由於它們各包含乙個對方物件的引用,因此它們的引用計數無法歸零,因此不會被**。如果僅僅使用引用計數法來管理記憶體,則會因為迴圈引用造成記憶體洩露

為了解決物件的迴圈引用問題,python引入了標記-清除和分代**兩種gc機制。

跟其名稱一樣,該演算法在進行垃圾**時分成了兩步,分別是:

標記階段,遍歷所有的物件,如果是可達的(reachable),也就是還有物件引用它,那麼就標記該物件為可達。

清除階段,再次遍歷物件,如果發現某個物件沒有標記為可達,則就將其**。

在標記清除演算法中,為了追蹤容器物件,需要每個容器物件維護兩個額外的指標,用來將容器物件組成乙個雙端鍊錶,指標分別指向前後兩個容器物件,方便插入和刪除操作。python直譯器(cpython)維護了兩個這樣的雙端鍊錶,乙個鍊錶存放著需要被掃瞄的容器物件,另乙個鍊錶存放著臨時不可達物件。

標記階段

gc第一次遍歷所有物件,複製每個物件的引用計數,可以記為gc_ref。假設,每個物件i,該計數為gc_ref_i。python會遍歷所有的物件i。對於每個物件i引用的物件j,將相應的gc_ref_j減1。這一步操作就相當於解除了迴圈引用對引用計數的影響。

接著,gc第二次遍歷所有的容器物件,如果物件的gc_ref值為0,那麼這個物件就被標記為unreachable;如果物件的gc_ref不為0,則被標記為reachable,並且會遞迴地將從該節點出發可以到達的所有節點標記為reachable

被標記為unreachable的物件會被移到unreachable鍊錶中

清除階段

**所有被標記為unreachable的物件

在標記-清除演算法執行的過程中,需要掃瞄整個記憶體空間,應用程式會被暫停,為了提公升工作效率,python採用了分代**的策略

弱代假說:年輕的物件通常消亡得快,而老物件則很可能存活更長時間。

python將所有物件分為0、1、2三代,他們對應的是3個鍊錶。

所有新建物件都是0代,當某一代物件經歷過垃圾**,依然存活,則被歸入下一代。

如果0代經歷一定次數的垃圾**,則會啟動對0代和1代的垃圾**;當1代也經歷了一定次數的垃圾**,則會啟動對0、1、2代的垃圾**

>>> import gc

>>> print(gc.get_threshold())

(700, 10, 10)

700是被分配的物件與被釋放的物件之差(分配記憶體的數目減去釋放記憶體的數目);後面兩個10,表示10次0代垃圾**後,才會執行一次0、1代的垃圾**;10次1代垃圾**後,才會執行一次0、1、2代的垃圾**

手動觸發垃圾**:

>>> print(gc.get_count())

(562, 10, 0)

>>> a=

>>> print(gc.get_count())

(563, 10, 0)

>>> gc.collect()

0>>> print(gc.get_count())

(22, 0, 0)

gc.collect(generation=2)

若被呼叫時不包含引數,則啟動完全的垃圾**。可以通過generation引數指定啟動哪一代的垃圾

參考資料

gc --- 垃圾**器介面

python中的垃圾**機制

聊聊python的記憶體管理

python記憶體分配

python深入06 python的記憶體管理

畫說 ruby 與 python 垃圾**

記憶體管理知識

記憶體管理向來是c c 程式設計的一塊雷區,大家都不怎麼願意去碰她,但是有時不得不碰它。雖然利用c 中的 art pointer已經可以完全避免使用指標,但是對於對於指標的進一步了解,有助於我們編寫出更有效率的 也有助於我們讀懂以前編寫的程式。五大記憶體分割槽 在c 中,記憶體分成5個區,他們分別是...

記憶體管理知識

記憶體管理向來是c c 程式設計的一塊雷區,大家都不怎麼願意去碰她,但是有時不得不碰它。雖然利用c 中的 art pointer已經可以完全避免使用指標,但是對於對於指標的進一步了解,有助於我們編寫出更有效率的 也有助於我們讀懂以前編寫的程式。五大記憶體分割槽 在c 中,記憶體分成5個區,他們分別是...

Mac OS 記憶體管理知識

先請看下面三張,是mac os系統的 活動監視器 的截圖,分別是8g,4g,2g記憶體。使用mac os系統時,關注記憶體的使用情況有時是必要的。常常使用windows系統的朋友,可能覺得mac os系統一啟動好像就一下子把記憶體全用光了,很緊張。其實是對mac os 或linux 系統的記憶體管理...