本文根据 撩课-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 来记录当前对应的引用计数;
对于每一个容器对象,找到它所引用的容器对象,并将这个容器对象的引用计数 -1;
通过步骤 3 后,若是一个容器对象的引用计数为 0,就表明这个对象能够被回收了,确定是 "循环引用" 才致使它活到如今的。
分代回收(如何提高查找循环引用的性能?)htm
若是程序中建立了不少个对象,而针对每个对象都要参与 检测 过程,则会很是的耗费性能,基于这个问题,Python 提出了一个假设,那就是:越命大的对象越长寿。
假设一个对象被检测 10 次都没有把它释放掉,就认定它必定很长寿,就减小对这个对象的 检测频率。
分代检测(基于假设设计出的一套检测机制)
默认一个对象被建立出来后,属于第 0 代;
若是经历过这一代 垃圾回收 后,依然存活,则划分到下一代;
垃圾回收的周期顺序
0 代 "垃圾回收" 必定次数后,触发 0~1 代回收;
1 代 "垃圾回收" 必定次数后,触发 0~2 代回收。
关于分代回收机制,它主要的做用是能够减小垃圾检测的频率。严格来讲,除了它有这个机制限定外,还有一个限定它的条件,那就是,在 垃圾回收器 中,当 "新增的对象个数 - 销毁的对象个数 = 规定阈值" 时才会去检测。
触发垃圾回收
自动回收
触发条件是,开启垃圾回收机制 ( 默认开启 ),而且达到了垃圾回收的阈值。
须要注意的是,触发并非检查全部的对象,而是分代回收。
手动回收 ( 默认0~2 )
只需执行 gc.collect(n) ,n 能够是 0~2,表示回收 0~n 代垃圾。
gc 模块
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)
修改垃圾检测频率。通常状况下,为了程序性能,会把这些数值调大。
测试自动回收 1
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,而在执行销毁操做时,是不会触发垃圾检测的,所以对象不被释放。
注意
此结论是我我的推测的,也有可能真是状况并非这样。我也是想了很久为何不释放对象,最终想到的一个比较合理的解释。
测试自动回收 2
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> 被释放
<__main__.dog object at> 被释放
p2 = <__main__.person object at>
----------------------- 程序结束 -----------------------
<__main__.person object at> 被释放
"""
复制代码
总共建立 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> 被释放
# <__main__.dog object at> 被释放
复制代码
弱引用
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> 被释放
# <__main__.dog object at> 被释放
复制代码
为证实一个对象被销毁时,它所引用对象的计数是否 -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
复制代码