本文根據 撩課-Python內存管理機制 整理而成。python
當一個對象被引用時,引用計數 +1,當這個對象再也不被引用,或引用它的對象被釋放時,引用計數 -1,當對象的引用計數爲 0 時,釋放該對象。函數
使用 sys.getrefcount(obj)
能夠查看一個對象的當前引用計數。在 Python 中,當對象被傳入到一個函數時,在這個函數的內部有會兩個對象引用着它。可是 sys.getrefcount(obj)
比較特殊,一般只引用一次。性能
class Person:
pass
def log(obj):
# obj += 2
print(sys.getrefcount(obj)) # obj += 1
p = Person() # p = 1
log(p) # p = 4
print(sys.getrefcount(obj)) # p = 2
複製代碼
對象在離開函數做用域時,會斷開和函數對象之間的引用,所以最後 p
的引用計數爲 2。測試
簡單來講,當一個對象再也不使用時,應該被釋放,可是,當對象被刪除後仍然存在引用計數時,將沒法釋放該對象。spa
class Person:
def __del__(self):
print("Person({0}) 被釋放".format(id(self)))
class Dog:
def __del__(self):
print("Dog({0}) 被釋放".format(id(self)))
p = Person() # p = 1
dog = Dog() # dog = 1
# 循環引用
p.pet = dog # dog = 2
dog.master = p # p = 2
# 程序結束前 __del__() 不被調用
# 因爲循環引用,本質上沒法真正刪除 p, dog,只是在語法層面上刪除了它們。
del p, dog # p, dog = 1, 1
複製代碼
在語法層面上,p
、dog
被刪除後就沒法再使用了,也沒法經過 p
、dog
的屬性 pet
和 master
來找到它們。 所以,將 p
、dog
稱之爲 可到達的引用,將 pet
、master
稱爲 不可到達的引用。也就是說,將 p
、dog
刪除後,雖然 pet
和 master
所引用的 dog
、p
還在內存中,可是已經沒法經過正常手段來訪問他們了,p
、dog
對象將在內存中沒法被釋放掉。設計
當被 del
後的對象還存在引用計數時,經過 引用計數器機制 就沒法作到真正從內存中回收它們,因而就形成了,由循環引用引發的內存泄漏問題。3d
""" 錯誤!未定義 p, dog print(p) print(dog) """
複製代碼
Python 由兩套內存管理機制並存,分別是 引用計數器機制 和 垃圾回收機制。引用計數器機制性能優於垃圾回收機制,可是沒法回收循環引用。所以,垃圾回收機制的主要做用在於,從 經歷過引用計數器機制後 仍未被釋放的對象中,找到循環引用並釋放掉相關對象。code
垃圾回收的底層機制(如何找到循環引用?)orm
list
, dict
, tuple
, customClass
, ...
) ,經過一個雙向鏈表進行引用;gc_refs
來記錄當前對應的引用計數;分代回收(如何提高查找循環引用的性能?)htm
若是程序中建立了不少個對象,而針對每個對象都要參與 檢測 過程,則會很是的耗費性能,基於這個問題,Python 提出了一個假設,那就是:越命大的對象越長壽。
假設一個對象被檢測 10 次都沒有把它釋放掉,就認定它必定很長壽,就減小對這個對象的 檢測頻率。
分代檢測(基於假設設計出的一套檢測機制)
垃圾回收的週期順序
關於分代回收機制,它主要的做用是能夠減小垃圾檢測的頻率。嚴格來講,除了它有這個機制限定外,還有一個限定它的條件,那就是,在 垃圾回收器 中,當 "新增的對象個數 - 銷燬的對象個數 = 規定閾值" 時纔會去檢測。
觸發垃圾回收
自動回收
觸發條件是,開啓垃圾回收機制 ( 默認開啓 ),而且達到了垃圾回收的閾值。
須要注意的是,觸發並非檢查全部的對象,而是分代回收。
手動回收 ( 默認0~2 )
只需執行 gc.collect(n)
,n
能夠是 0~2,表示回收 0~n 代垃圾。
gc
模塊能夠查看或修改 垃圾回收器 當中的一些信息。
import gc
複製代碼
gc.isenabled()
判斷垃圾回收器機制是否開啓。
gc.enable()
開啓垃圾回收器機制 ( 默認開啓 ) 。
gc.disable()
關閉垃圾回收器機制。
gc.get_threshold()
獲取觸發執行垃圾檢測閾值,返回值是一個元組 ( threshold, n1, n2 )
。
threshold
就是觸執行發垃圾檢測的閾值,當 新增的對象個數 - 銷燬的對象個數 = threshold
時,執行一次垃圾檢測。
n1
表示當 0 代垃圾檢測達到 n1 次時,觸發 0~1 代垃圾回收。
n2
表示當 1 代垃圾檢測達到 n2 次時,觸發 1~2 代垃圾回收。
gc.set_threshold(1000, 15, 15)
修改垃圾檢測頻率。通常狀況下,爲了程序性能,會把這些數值調大。
import gc
# "建立對象的次數 - 銷燬對象的次數 = 2" 時,觸發自動回收。
gc.set_threshold(2, 10, 10)
class Person:
def __del__(self):
print(self, "被釋放")
class Dog:
def __del__(self):
print(self, "被釋放")
p = Person() # p = 1
dog = Dog() # dog = 1
# 循環引用
p.pet = dog # dog = 2
dog.master = p # p = 2
# 多建立一個 Person 類,目的是爲測試在刪除對象後,程序可以觸發自動回收。
p2 = Person()
# 程序結束前,不調用 __del__()。
del p
del dog
複製代碼
總共建立 3 個對象,銷燬了 1 個對象,3-1=2。理論上說,此時應該觸發自動回收,但直到程序結束以前,__del__()
函數都沒有被調用,這是爲何呢?
要解釋這個問題,首先就要了解,爲何垃圾檢測會存在 "新增的對象個數 - 銷燬的對象個數 = 規定閾值"
這樣一個限定條件。
這是由於,當對象遺留在內存中沒法被釋放時,緣由一般是對象建立多了而沒有被及時銷燬的緣由。
那麼根據這個結論,就能夠設定一個機制,當 "建立的對象" 多出 "被銷燬的對象" 大於或等於 "指定閾值" 時,再讓程序去檢測垃圾回收,不然不觸發檢測。
在銷燬一個對象時,表現的是,將減小一次達到指定閾值的條件,也就沒有必要再去檢測了。
因此嚴格來講,這個限定條件要改爲:在建立對象時,"新增的對象個數 - 銷燬的對象個數 = 規定閾值" 時
,觸發垃圾檢測。
瞭解了這些以後,你就知道,爲何這裏對象沒法被釋放了。首先建立了 3 個對象,而後執行 del p
、del dog
,而在執行銷燬操做時,是不會觸發垃圾檢測的,所以對象不被釋放。
注意
此結論是我我的推測的,也有可能真是狀況並非這樣。我也是想了很久爲何不釋放對象,最終想到的一個比較合理的解釋。
import gc
gc.set_threshold(2, 10, 10)
class Person:
def __del__(self):
print(self, "被釋放")
class Dog:
def __del__(self):
print(self, "被釋放")
p = Person() # p = 1
dog = Dog() # dog = 1
# 循環引用
p.pet = dog # dog = 2
dog.master = p # p = 2
# 嘗試在刪除 "可到達引用" 後,真實對象是否有被回收。
del p, dog
# 多建立一個 Person 類,目的是爲測試在刪除對象後,程序可以觸發自動回收。
p2 = Person()
print("p2 =", p2)
print("----------------------- 程序結束 -----------------------")
""" <__main__.Person object at 0x0000000002c28190> 被釋放 <__main__.Dog object at 0x0000000002cf33d0> 被釋放 p2 = <__main__.Person object at 0x0000000002cf3350> ----------------------- 程序結束 ----------------------- <__main__.Person object at 0x0000000002cf3350> 被釋放 """
複製代碼
總共建立 5 個對象,銷燬了 3 個對象,5-3=2,觸發自動檢測。此時發現 p
, g
已被銷燬 ( 真實對象還在內存中 ),因而找到它們所引用的對象,將計數 -1,p
、dog
得以被釋放。
注意:是
p
、dog
先被釋放,p2
在程序結束後被釋放。
import gc
class Person:
def __del__(self):
print(self, "被釋放")
class Dog:
def __del__(self):
print(self, "被釋放")
p = Person() # p = 1
dog = Dog() # dog = 1
# 循環引用
p.pet = dog # dog = 2
dog.master = p # p = 2
del p # p = 1
del dog # dog = 1
# 對程序執行垃圾檢測 (無關回收機制是否開啓),手動回收內存。
gc.collect()
# <__main__.Person object at 0x109cb0110> 被釋放
# <__main__.Dog object at 0x109cb0190> 被釋放
複製代碼
import weakref
import sys
class Person:
def __del__(self):
print(self, "被釋放")
class Dog:
def __del__(self):
print(self, "被釋放")
p = Person() # p = 1
dog = Dog() # dog = 1
p.pet = dog # dog = 2
# weakref.ref 不強引用指定對象 (即不增長引用計數)。
dog.master = weakref.ref(p) # p = 1
# p 被徹底銷燬時,它所引用對象的計數 -1.
del p # p = 0, dog = 1
del dog # dog = 0
# <__main__.Person object at 0x109cb0110> 被釋放
# <__main__.Dog object at 0x109cb0190> 被釋放
複製代碼
爲證實一個對象被銷燬時,它所引用對象的計數是否 -1,特此作個實驗,來觀察 p
被銷燬時,它所指向的 dog
引用計數。
p.pet = dog # dog = 2
dog.master = weakref.ref(p) # p = 1
del p # p = 0, dog = 1
""" 觀察 p 被銷燬時,它所引用的 dog 計數是否被 -1 sys.getrefcount 用於獲取一個對象的當前引用計數,返回值比實際值多 1。 """
print(sys.getrefcount(dog)) # 2
del dog # dog = 0
複製代碼
當 p
被銷燬時,意味着在 p.pet = god
這條語句中,前面的 p
、p.pet
已經不存在了,只剩下 = dog
,前面空空如也,並不被任何對象所引用,所以 dog
的引用計數 -1。
而在強引用下,p
被銷燬時,dog
的引用計數不變。
p.pet = dog # dog = 2
dog.master = p # p = 2
del p # p = 1, dog = 2
print(sys.getrefcount(dog)) # 3,實際值爲 2.
del dog # dog = 1
複製代碼
要在一個集合中弱引用對象,使用 weakref.Weak...
。
# 弱所引用字典中的對象
# pets = weakref.WeakValueDictionary({"dog": d1, "cat": c1})
複製代碼
class Person:
def __del__(self):
print(self, "被釋放")
class Dog:
def __del__(self):
print(self, "被釋放")
p = Person() # p = 1
dog = Dog() # dog = 1
p.pet = dog # dog = 2
dog.master = p # p = 2
""" 在刪除前手動打破循環引用 這意味着手動斷開 p.pet 與 dog 之間的引用, 當 dog 再也不被 p 引用時,計數天然 -1。 """
p.pet = None
del p # p = 0, dog = 1
del dog # dog = 0
複製代碼