python散装笔记——111: 垃圾回收
1: 原始对象的重用
一个有助于优化应用程序的有趣现象是,原始类型实际上在底层也是通过引用计数来管理的。我们来看看数字:对于所有介于 -5 和 256 之间的整数,Python 总是重用同一个对象:
>>> import sys
>>> sys.getrefcount(1)
1262
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
1433
请注意引用计数增加了,这意味着当 a 和 b 指向 1 这个原始类型时,它们引用的是同一个底层对象。然而,对于更大的数字,Python 实际上并不会重用底层对象:
>>> a = 999999999
>>> sys.getrefcount(999999999)
2
>>> b = 999999999
>>> sys.getrefcount(999999999)
2
由于将 999999999 分配给 a 和 b 时引用计数没有变化,我们可以推断它们指向两个不同的底层对象,尽管它们都被分配了相同的原始类型。
2: del命令的效果
使用 del v 从作用域中移除变量名,或者使用 del v[item] 或 del[i:j] 从集合中移除对象,或者使用 del v.name 移除属性,或者其他任何移除对对象引用的方式,并不会直接触发任何析构函数调用或释放任何内存。只有当对象的引用计数达到零时,对象才会被析构。
>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> def bar():
return Track()
>>> t = bar()
Initialized
>>> another_t = t # 分配另一个引用
>>> print("...")
...
>>> del t # 尚未析构——`another_t` 仍然引用它
>>> del another_t # 最后一个引用被移除,对象被析构
Destructed
3: 引用计数
Python 的大部分内存管理是通过引用计数来处理的。
每当一个对象被引用(例如被分配给一个变量)时,它的引用计数会自动增加。当它被取消引用(例如变量超出作用域)时,它的引用计数会自动减少。
当引用计数达到零时,对象会立即被销毁,内存也会立即被释放。因此,在大多数情况下,垃圾回收器甚至都不是必需的。
>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> def foo():
Track()
# 由于不再有任何引用,立即被析构
print("---")
t = Track()
# 变量被引用,因此尚未被析构
print("---")
# 当函数退出时,变量被析构
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed
为了进一步说明引用的概念:
>>> def bar():
return Track()
>>> t = bar()
Initialized
>>> another_t = t # 分配另一个引用
>>> print("...")
...
>>> t = None # 尚未析构——`another_t` 仍然引用它
>>> another_t = None # 最后一个引用被移除,对象被析构
Destructed
4: 处理引用循环的垃圾回收器
垃圾回收器只有在存在引用循环时才是必需的。最简单的引用循环例子是 A 指向 B,而 B 指向 A,同时没有任何其他对象指向 A 或 B。A 和 B 都无法从程序的任何地方访问,因此可以安全地被析构,但由于它们的引用计数为 1,因此仅靠引用计数算法无法释放它们。
>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B # 由于引用循环,对象尚未被析构
>>> gc.collect() # 触发回收
Destructed
Destructed
20
引用循环可以任意长。如果 A 指向 B 指向 C 指向……指向 Z,而 Z 又指向 A,那么从 A 到 Z 都不会被回收,直到垃圾回收阶段:
>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
... objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0] # 完成循环
>>> del objs # 现在没有任何对象引用 `objs`——但仍未被析构
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20
5: 强制释放对象
在 Python 2 和 3 中,即使对象的引用计数不为 0,你也可以强制释放对象。
两个版本都使用 ctypes 模块来实现。
警告:这样做会使你的 Python 环境变得不稳定,容易在没有堆栈跟踪的情况下崩溃!使用这种方法还可能引入安全问题(虽然不太可能)。只有当你确定你永远不会再引用某个对象时,才应该释放它。
>>> import ctypes
>>> deallocated = 12345
>>> ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
1440518144
运行后,对刚刚释放的对象的任何引用都将导致 Python 产生未定义行为或崩溃——而且不会显示堆栈跟踪。垃圾回收器没有移除那个对象,可能有其原因……
如果你释放了 None,你会收到一条特殊的消息——在崩溃之前显示“致命的 Python 错误:释放 None”。
111.6 节:查看对象的引用计数
>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2
111.7: 不要等待垃圾回收来清理
垃圾回收会清理内存这一事实并不意味着你应该等待垃圾回收周期来清理。
特别是你不应该等待垃圾回收来关闭文件句柄、数据库连接和打开的网络连接。
例如:
在下面的代码中,你假设文件将在下一个垃圾回收周期中被关闭,如果 f 是对文件的最后一个引用。
>>> f = open("test.txt")
>>> del f
一种更明确的清理方式是调用 f.close()。你可以更优雅地实现这一点,即使用 with 语句,也称为上下文管理器:
>>> with open("test.txt") as f:
... pass
... # 对 `f` 进行一些操作
>>> # 现在 `f` 对象仍然存在,但它已经被关闭了
with 语句允许你将代码缩进到打开的文件下。这使得代码更明确,更容易看出文件保持打开的时间有多长。即使在 with 块中引发异常,它也会始终关闭文件。
8: 管理垃圾回收
有两种方法可以影响何时执行内存清理。一种是影响自动进程的执行频率,另一种是手动触发清理。
可以通过调整回收阈值来操纵垃圾回收器,这些阈值会影响回收器运行的频率。Python 使用基于代的内存管理系统。新对象被保存在最新的代中——generation0,并且每次在回收中存活下来后,对象都会被提升到更老的代中。
达到最后一代——generation2 后,它们不再被提升。
可以使用以下代码片段更改阈值:
import gc
gc.set_threshold(1000, 100, 10) # 这些值仅用于演示目的
第一个参数表示收集 generation0 的阈值。每当分配的内存数量超过释放的内存数量 1000 时,垃圾回收器就会被调用。
为了避免每次运行时都清理较老的代以优化进程,清理较老代的频率可以通过第二和第三个参数控制,这两个参数是可选的。如果在没有清理 generation1 的情况下处理了 generation0 100 次,则会处理 generation1。同样,只有在没有清理 generation2 的情况下清理了 generation1 10 次时,才会处理 generation2 中的对象。
在程序分配了大量小对象且没有释放它们时,手动设置阈值是有益的,这会导致垃圾回收器运行得太频繁(每次分配 generation0_threshold 个对象时)。尽管回收器本身速度很快,但当它处理大量对象时,可能会出现性能问题。不过,没有一种通用的策略可以用于选择阈值,这取决于具体用例。
可以使用以下代码片段手动触发回收:
import gc
gc.collect()
垃圾回收是根据分配和释放的次数自动触发的,而不是根据消耗或可用内存量。因此,在处理大对象时,可能会在自动清理被触发之前耗尽内存。这为手动调用垃圾回收器提供了一个很好的用例。
尽管这是可能的,但这并不是一个被鼓励的做法。避免内存泄漏才是最佳选择。然而,在大型项目中,检测内存泄漏可能是一项艰巨的任务,手动触发垃圾回收可以作为一种快速解决方案,直到进一步调试。
对于长期运行的程序,可以根据时间或事件触发垃圾回收。第一个例子是,一个 web 服务器在固定数量的请求后触发回收。对于后者,一个 web 服务器在接收到某种类型的请求时触发垃圾回收。