引用计数
Python 的垃圾收集(GC)策略主要依赖于引用计数,它会自动地管理和回收不再使用的内存。Python 给所有的对象都维护了一个引用计数的属性,当一个引用被创建或复制时,相关对象的引用计数+1;相反当销毁的时候,引用计数-1。当一个 Python 对象的引用计数变为 0 时,它就会被立即删除,其占用的内存会被释放。
好处是,相较于 Java 固定时间上进行 GC,Python 可以动态的进行内存管理。
缺点是,创建和销毁会带来效率上的不足。
循环引用
引用计数还存在一个致命的问题就是无法对循环引用进行处理。
import sys
class Test():
def __init__(self):
pass
t = Test()
k = Test()
t._self = t
print(sys.getrefcount(t)) # 3
print(sys.getrefcount(k)) # 2getrefcount 函数查看一个对象存在几个引用关系,一般状态下的普通变量如上面的 k,返回值都是 2。不是 1 是因为把 k 作为参数传递给函数的时候,要先复制一份引用,然后把这个引用赋给形式参数供函数运行,在函数运行过程中,会保持这个引用始终升高为 2。
标记-清除的回收机制
标记清除算法是一种基于对象可达性的垃圾回收算法。它通过从根对象(如全局变量、活动函数等)出发,标记所有能够被引用到的对象,然后清除未标记的对象。这样就能够释放那些不再被引用的对象占用的内存空间。
针对循环引用这个问题,比如有两个对象互相引用了对方,当外界没有对他们有任何引用,也就是说他们各自的引用计数都只有 1 的时候,如果可以识别出这个循环引用,把它们属于循环的计数减掉的话,就可以看到他们的真实引用计数了。基于这样一种考虑,有一种方法,比如从对象 A 出发,沿着引用寻找到对象 B,把对象 B 的引用计数减去 1;然后沿着 B 对 A 的引用回到 A,把 A 的引用计数减 1,这样就可以把这层循环引用关系给去掉了。
不过这么做还有一个考虑不周的地方。假如 A 对 B 的引用是单向的,在到达 B 之前我不知道 B 是否也引用了 A,这样子先给 B 减 1 的话就会使得 B 称为不可达的对象了。为了解决这个问题,python 中常常把内存块一分为二,将一部分用于保存真的引用计数,另一部分拿来做为一个引用计数的副本,在这个副本上做一些实验。比如在副本中维护两张链表,一张里面放不可被回收的对象合集,另一张里面放被标记为可以被回收(计数经过上面所说的操作减为 0)的对象,然后再到后者中找一些被前者表中一些对象直接或间接单向引用的对象,把这些移动到前面的表里面。这样就可以让不应该被回收的对象不会被回收,应该被回收的对象都被回收了。
分代回收
分代回收是一种高效的垃圾回收策略,通常应用于长时间运行的程序中,如服务器端应用或长期运行的桌面应用。它的核心思想是基于对象的生命周期将内存分为不同的代(Generation),并针对每一代采取不同的回收策略。
一般来说,程序中的对象可以分为三个代:
- 年轻代(Young Generation):包含了刚刚创建的对象。通常情况下,大部分对象在创建之后很快就会变成垃圾。因此,年轻代中的对象存活时间较短。
- 中年代(Intermediate Generation):包含了经过一定时间仍然存活的对象。这些对象可能被多次引用,但仍不具备长期存活的特征。
- 老年代(Old Generation):包含了存活时间较长的对象,通常是经过多次垃圾回收仍然存活的对象。这些对象往往是程序中的核心数据结构或全局对象,它们的存活时间相对较长。
分代回收基于一种观察:大部分对象的生命周期都是短暂的,只有少数对象会长时间存活。因此,分代回收将重点放在年轻代上,采用更频繁的、轻量级的垃圾回收策略,如快速的引用计数、标记-清除等。对于存活时间较长的对象,则采用更复杂、耗时较长的回收策略,如标记-整理、分代清理等。
通过这种分代的方式,垃圾回收器可以更加高效地管理内存,减少回收整个堆内存的频率,提高程序的性能和响应速度。
# 查看分代回收的阈值
print(gc.get_threshold()) # 输出:(700, 10, 10)这个方法返回的是(700,10,10),这也是 gc 的默认值。这个值的意思是说,在第 0 代对象数量达到 700 个之前,不把未被回收的对象放入第一代;而在第一代对象数量达到 10 个之前也不把未被回收的对象移到第二代。可以是使用 gc.set_threshold(threashold0,threshold1,threshold2) 来手动设置这组阈值。
弱引用
在 Python 中,弱引用(Weak Reference)是一种特殊类型的引用,它不会增加对象的引用计数。换句话说,弱引用不会阻止其引用的对象被垃圾回收器回收。
弱引用通常用于解决循环引用导致的内存泄漏问题,其中两个或多个对象之间互相引用,但又没有被其他地方引用。在这种情况下,即使这些对象已经不再被程序需要,它们仍然无法被垃圾回收,因为它们之间的循环引用导致引用计数不为零。
Python 的 weakref 模块提供了对弱引用的支持。以下是一些常见的用法和函数:
weakref.ref(object, callback=None)
- 创建一个对对象的弱引用,并返回一个弱引用对象。
- 可以通过
callback参数指定一个回调函数,在对象被垃圾回收时调用该函数。
weakref.proxy(object, callback=None)
- 创建一个对对象的弱引用代理,并返回一个代理对象。
- 与
ref()不同的是,通过代理对象可以像访问原始对象一样访问其属性和方法。
weakref.getweakrefs(object)
- 返回与对象相关联的所有弱引用的列表。
weakref.getweakrefcount(object)
- 返回与对象相关联的弱引用的数量。
弱引用对象的 .weakref 属性
- 对象被弱引用时,会自动创建一个
.weakref属性,该属性保存着对弱引用的引用。
示例用法:
import weakref
class Player:
def __init__(self, name):
self.name = name
self.friends = weakref.WeakSet() # 使用弱引用集合保存朋友列表
def add_friend(self, friend):
self.friends.add(friend)
def remove_friend(self, friend):
self.friends.discard(friend)
def __repr__(self):
return f"Player({self.name})"
# 创建玩家对象
player1 = Player("Alice")
player2 = Player("Bob")
player3 = Player("Charlie")
# 添加朋友关系
player1.add_friend(player2)
player2.add_friend(player1)
player2.add_friend(player3)
player3.add_friend(player2)
# 打印玩家及其朋友列表
print(f"{player1} has friends: {[friend.name for friend in player1.friends]}")
print(f"{player2} has friends: {[friend.name for friend in player2.friends]}")
print(f"{player3} has friends: {[friend.name for friend in player3.friends]}")
# 删除玩家对象
del player2
# 打印剩余玩家及其朋友列表
print(f"After deleting player2:")
print(f"{player1} has friends: {[friend.name for friend in player1.friends]}")
print(f"{player3} has friends: {[friend.name for friend in player3.friends]}")垃圾回收相关函数
sys.getrefcount 函数查看引用个数
del 删除变量名到对象的引用。
gc.collect 处理循环引用。
import sys
import gc
a = [1]
b = [2]
a.append(b)
b.append(a)
####此时a和b之间存在循环引用####
sys.getrefcount(a) #结果应该是3
sys.getrefcount(b) #结果应该是3
del a
del b
####删除了变量名a,b到对象的引用,此时引用计数应该减为1,即只剩下互相引用了####
try:
sys.getrefcount(a)
except UnboundLocalError:
print('a is invalid')
####此时,原来a指向的那个对象引用不为0,python不会自动回收它的内存空间####
####但是我们又没办法通过变量名a来引用它了,这就导致了内存泄露####
unreachable_count = gc.collect()
####gc.collect()专门用来处理这些循环引用,返回处理这些循环引用一共释放掉的对象个数。这里返回是2####然而,gc.collect 并不一定是万能的。例如下面就无法进行垃圾回收。
class A():
def __init__(self):
pass
def __del__(self):
pass
class B():
def __init__(self):
pass
def __del__(self):
pass
a = A()
b = B()
a._b = b
b._a = a
del a
del b
print(gc.collect()) #结果是4
print(gc.garbage) #结果是[<__main__.A instance at 0x0000000002296448>, <__main__.B instance at 0x0000000002296488>]这里需要明确一下,之前对于“垃圾”二字的定义并不是很明确,在这里的这个语境下,垃圾是指在经过 collect 的垃圾回收之后仍然保持 unreachable 状态,即无法被回收,且无法被用户调用的对象应该叫做垃圾。gc 模块中有 garbage 这个属性,其为一个列表,每一项都是当前解释器中存在的垃圾对象。一般情况下,这个属性始终保持为空集。
那么为什么在这种场景下 collect 不起作用了呢?这主要是因为我们在类中重载了 __del__ 方法。__del__ 方法指出了在用 del 语句删除对象时除了释放内存空间以外的操作。一般而言,在使用了 del 语句的时候解释器会首先看要删除对象的引用计数,如果为 0,那么就释放内存并执行 __del__ 方法。在这里,首先 del 语句出现时本身引用计数就不为 0(因为有循环引用的存在),所以解释器不释放内存;再者,执行 collect 方法时照理由应该会清除循环引用所产生的无效引用计数从而达到del的目的,对于这两个对象而言,python无法判断调用它们的 __del__ 方法时会不会要用到对方那个对象,比如在进行 b.__del__() 时可能会用到 b._a 也就是 a,如果在那之前 a 已经被释放,那么就彻底 GG 了。为了避免这种情况,collect 方法默认不对重载了 __del__ 方法的循环引用对象进行回收,而它们俩的状态也会从 unreachable 转变为 uncollectable。由于是 uncollectable 的,自然就不会被 collect 处理,所以就进入了 garbage 列表。
collect返回4的原因是因为,在A和B类对象中还默认有一个 __dict__ 属性,里面有所有属性的信息。比如对于a,有 a.__dict__ = {'_b':<__main__.B instance at xxxxxxxx>}。a的 __dict__ 和 b 的 __dict__ 也是循环引用的。但是字典类型不涉及自定义的 __del__ 方法,所以可以被 collect 掉。所以 garbage 里只剩下两个了。
调试内存泄露
objgraph,一个非常好用的可视化引用关系的包。在这个包中,我主要推荐两个函数,第一个是 show_refs(),它可以生成清晰的引用关系图。
通过下面这段代码和生成的引用调用图,你能非常直观地发现,有两个 list 互相引用,说明这里极有可能引起内存泄露。这样一来,再去代码层排查就容易多了。
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
而另一个非常有用的函数,是 show_backrefs()。
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_backrefs([a])