當建立物件時, 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 系統的記憶體管理...