python垃圾回收 (GC) 机制
Python 能夠自動(dòng)進(jìn)行內(nèi)存分配和釋放,但了解 python 垃圾回收 (garbage collection, GC) 的工作原理可以幫助你寫(xiě)出更好更快的 Python 程序。Python 使用兩種算法進(jìn)行垃圾回收,分別是引用計(jì)數(shù) (Reference Counting) 和分代回收 (Generational garbage collection)。
引用計(jì)數(shù)
引用計(jì)數(shù),簡(jiǎn)而言之就是如果沒(méi)有變量引用某一對(duì)象,那么該對(duì)象將會(huì)被回收。Python 中的每個(gè)變量都是對(duì)對(duì)象的引用,而不是對(duì)象本身。例如,賦值語(yǔ)句只是給右側(cè)對(duì)象或右側(cè)變量所對(duì)應(yīng)的對(duì)象建立一個(gè)引用;一個(gè)對(duì)象都可以有許多引用。
核心概念:變量是指向一個(gè)對(duì)象的指針;有n個(gè)變量指向某一個(gè)對(duì)象,那該對(duì)象的引用計(jì)數(shù)則為n,又稱該對(duì)象有n個(gè)引用
import sys a = [1, 2, 3] b = a # 賦值操作本身不會(huì)對(duì)數(shù)據(jù)進(jìn)行復(fù)制,僅僅是建立引用關(guān)系print(id(a), id(b)) # 4483101696 4483101696,變量a b的id相同,說(shuō)明a b指向同一對(duì)象 print(sys.getrefcount(a)) # 3, 其中調(diào)用getrefcount函數(shù)會(huì)使引用+1為了跟蹤每個(gè)對(duì)象的引用次數(shù),每個(gè)對(duì)象都有一個(gè)名為引用計(jì)數(shù)的額外屬性,當(dāng)創(chuàng)建或刪除指向?qū)ο蟮闹羔槙r(shí),該屬性的值會(huì)相應(yīng)的增加或減少。以下三種情況會(huì)使對(duì)象的引用次數(shù)增加:
- 賦值運(yùn)算
- 參數(shù)傳遞
- 將對(duì)象附加到容器對(duì)象中
??如果某對(duì)象引用計(jì)數(shù)屬性的值為零,CPython 會(huì)自動(dòng)調(diào)用該對(duì)象特定的內(nèi)存釋放函數(shù)。如果該對(duì)象還包含對(duì)其他對(duì)象的引用,那么所包含的其他對(duì)象的引用計(jì)數(shù)也會(huì)自動(dòng)減少。因此,可以依次釋放其他對(duì)象。
??值得注意的是,在函數(shù)、類和代碼塊(如if-else代碼塊)之外聲明的變量稱為全局變量(global variables)。通常,這些變量會(huì)一直存在直到 Python 進(jìn)程結(jié)束。因此,全局變量引用的對(duì)象的引用計(jì)數(shù)永遠(yuǎn)不會(huì)下降到零。在python進(jìn)程中,所有全局變量都存儲(chǔ)在一個(gè)字典中,可以通過(guò)調(diào)用globals()函數(shù)來(lái)獲取全局變量。那反過(guò)來(lái)呢?在代碼塊內(nèi)(例如,在函數(shù)或類中)定義的變量則具有一個(gè)局部作用域,可以通過(guò)調(diào)用locals()函數(shù)來(lái)獲取局部變量。當(dāng) Python 解釋器執(zhí)行完一個(gè)代碼塊時(shí),它會(huì)破壞在塊內(nèi)創(chuàng)建的局部變量及其引用。
我們舉個(gè)例子:
import sysfoo = [] print(sys.getrefcount(foo)) # 2 references, 1 from the foo var and 1 from getrefcountdef bar(a):print(sys.getrefcount(a))bar(foo) # 4 references, from the foo var, function argument, getrefcount and Python's function stack print(sys.getrefcount(foo)) # 2 references, the function scope is destroyed當(dāng)你想刪除全局或局部變量時(shí),可以使用刪除變量及其引用(而不是對(duì)象本身)的 del語(yǔ)句。這在 jupyter notebook中工作時(shí)通常很有用,因?yàn)樵趈upyter notebook中所有單元格變量都是全局變量。CPython 使用引用計(jì)數(shù)的主要原因是歷史原因,現(xiàn)在有很多關(guān)于這種技術(shù)的弱點(diǎn)的爭(zhēng)論。比如,有人認(rèn)為現(xiàn)代的垃圾回收算法可以更高效,無(wú)需使用引用計(jì)數(shù)。引用計(jì)數(shù)算法存在很多問(wèn)題,例如循環(huán)引用、線程鎖定以及額外內(nèi)存和性能開(kāi)銷(xiāo)。必須指出的是,引用計(jì)數(shù)是 Python 無(wú)法擺脫全局解釋鎖 (GIL) 的原因之一。
分代回收
上面講到了引用計(jì)數(shù)的缺點(diǎn)包括循環(huán)引用、線程鎖定以及額外內(nèi)存和性能開(kāi)銷(xiāo)。線程鎖定,所以在python中使用多線程;額外內(nèi)存和性能開(kāi)銷(xiāo),我們也認(rèn)了;但是循環(huán)引用的問(wèn)題不解決的話,就會(huì)造成內(nèi)存泄露問(wèn)題。因此,python引入了分代回收的算法專門(mén)來(lái)解決循環(huán)引用的問(wèn)題。
引用計(jì)數(shù)算法非常有效和直接,但它無(wú)法檢測(cè)循環(huán)引用,所以python在引用計(jì)數(shù)的基礎(chǔ)上,還需要分代回收。引用計(jì)數(shù)是 Python必需的功能,不能禁用;而分代回收是可選的,可以手動(dòng)設(shè)置。
如上圖示例所示,lst對(duì)象指向自身,而且Object 1 和 Object 2 相互引用。在這兩種情況下,這些對(duì)象的引用數(shù)永遠(yuǎn)至少為1。我們可以用代碼來(lái)演示一下:
import gc import sys import ctypes# 通過(guò)內(nèi)存地址去訪問(wèn)沒(méi)有引用的對(duì)象(unreachable objects) class PyObject(ctypes.Structure):_fields_ = [("refcnt", ctypes.c_long)]gc.disable() # 禁用分代回收算法 lst = [] lst.append(lst) lst_address = id(lst) del lstobject_1, object_2 = {}, {}object_1['obj2'] = object_2 object_2['obj1'] = object_1 obj_address = id(object_1) del object_1, object_2# 手動(dòng)對(duì)象回收 # gc.collect()# 獲取對(duì)象引用數(shù)量 print(PyObject.from_address(obj_address).refcnt) print(PyObject.from_address(lst_address).refcnt)# 或者通過(guò)以下方式獲取引用數(shù)量 # tmp = PyObject.from_address(obj_address) # print(sys.getrefcount(tmp))# # output 1 1 2在上面的示例中,del語(yǔ)句刪除了對(duì)我們對(duì)象的引用(引用計(jì)數(shù)減 1)。 Python 執(zhí)行 del語(yǔ)句后,我們的對(duì)象不再可以從 Python 代碼訪問(wèn)。但是,這些對(duì)象仍然存在于內(nèi)存中。發(fā)生這種情況是因?yàn)樗鼈內(nèi)栽谙嗷ヒ?#xff0c;并且每個(gè)對(duì)象的引用計(jì)數(shù)為 1。因?yàn)槲覀兦懊嫱ㄟ^(guò)gc.disable()禁用了分代回收,因?yàn)檠h(huán)引用對(duì)象無(wú)法釋放;這時(shí),我們可以通過(guò)調(diào)用gc.collect()手動(dòng)觸發(fā)對(duì)象回收。
??我們知道,在python中對(duì)象分為可變對(duì)象和不可變對(duì)象。不可變對(duì)象包括int, float, complex, strings, bytes, tuple, range 和 frozenset;可變對(duì)象包括list, dict, bytearray和 set。循環(huán)引用僅存在于container對(duì)象(比如,list, dict, classes, tuples),python垃圾回收算法主要追蹤可變對(duì)象及不可變對(duì)象tuple。如果tuple, dict包含的元素都是不可變對(duì)象,那么回收算法可以不對(duì)該對(duì)象進(jìn)行追蹤。
垃圾回收的觸發(fā)
不同于引用計(jì)數(shù),循環(huán)引用的垃圾回收不是實(shí)時(shí)作用的,而是定期運(yùn)行。垃圾回收器將container對(duì)象分成三代(0, 1, 2),每個(gè)新對(duì)象都從第一代開(kāi)始。如果一個(gè)對(duì)象在一個(gè)垃圾回收輪次中幸存下來(lái),它將移至較舊(更高)的一代。較低代的回收頻率高于較高代,因?yàn)榇蠖鄶?shù)新對(duì)象往往會(huì)被先銷(xiāo)毀。這樣分代回收的策略能提高性能并減少垃圾回收帶來(lái)的暫停時(shí)間。
??為了決定何時(shí)進(jìn)行一輪垃圾回收,每一代都有一個(gè)單獨(dú)的計(jì)數(shù)器和閾值。計(jì)數(shù)器存儲(chǔ)自上次收集以來(lái)的對(duì)象分配數(shù)減去釋放數(shù)的差值。每次分配新的容器對(duì)象時(shí),CPython 都會(huì)檢查第0代的計(jì)數(shù)器是否超過(guò)閾值(通過(guò)gc.get_count()獲得三代對(duì)象計(jì)數(shù)器存儲(chǔ)的數(shù)值)。如果超過(guò)閾值,Python 將觸發(fā)垃圾回收。我們可以通過(guò)gc.get_threshold()和gc.set_threshold()查看、設(shè)置閾值:
import gc gc.get_threshold() # (700, 10, 10) 分別對(duì)應(yīng)三代計(jì)數(shù)器的閾值 gc.set_threshold(threshold0=800, threshold0=10, threshold0=10) # 當(dāng)threshold0設(shè)置為0時(shí),禁用循環(huán)GC??在寫(xiě)程序時(shí),可以通過(guò)將調(diào)試標(biāo)志設(shè)置為gc.DEBUG_SAVEALL,從而將所有unreachable對(duì)象添加到gc.garbage 中,幫助提升程序質(zhì)量:
import gcgc.set_debug(gc.DEBUG_SAVEALL)print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage:print(item)參考
python documentation: GC
Garbage collection in Python: things you need to know
stacloverflow reference count
總結(jié)
以上是生活随笔為你收集整理的python垃圾回收 (GC) 机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在jupyter界面误删了jupyter
- 下一篇: 使用python调用matlab方法