引用计数器机制
当一个对象被引用时,引用计数 +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。
循环引用
简单来说,当一个对象不再使用时,应该被释放,但是,当对象被删除后仍然存在引用计数时,将无法释放该对象。
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 后的对象还存在引用计数时,通过 引用计数器机制 就无法做到真正从内存中回收它们,于是就造成了,由循环引用引起的内存泄漏问题。
"""
错误!未定义 p, dog
print(p)
print(dog)
"""
复制代码
垃圾回收机制
Python 由两套内存管理机制并存,分别是 引用计数器机制 和 垃圾回收机制。引用计数器机制性能优于垃圾回收机制,但是无法回收循环引用。因此,垃圾回收机制的主要作用在于,从 经历过引用计数器机制后 仍未被释放的对象中,找到循环引用并释放掉相关对象。
垃圾回收的底层机制(如何找到循环引用?)
收集所有 容器对象 ( list , dict , tuple , customClass, ... ) ,通过一个双向链表进行引用;
针对每一个容器对象,通过一个变量 gc_refs 来记录当前对应的引用计数;
对于每个容器对象,找到它所引用的容器对象,并将这个容器对象的引用计数 -1;
经过步骤 3 后,如果一个容器对象的引用计数为 0,就代表这个对象可以被回收了,肯定是 "循环引用" 才导致它活到现在的。
分代回收(如何提升查找循环引用的性能?)
如果程序中创建了很多个对象,而针对每一个对象都要参与 检测 过程,则会非常的耗费性能,基于这个问题,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 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
复制代码