Python 内存管理

为提升执⾏行性能,Python 在内存管理上做了⼤大量⼯工作。最直接的做法就是⽤用内存池来减少操作系
统内存分配和回收操作,那些⼩小于等于 256 字节对象,将直接从内存池中获取存储空间。
根据需要,虚拟机每次从操作系统申请⼀一块 256KB,取名为 arena 的⼤大块内存。并按系统⻚页⼤大
⼩小,划分成多个 pool。每个 pool 继续分割成 n 个⼤大⼩小相同的 block,这是内存池最⼩小存储单位。
14
block ⼤大⼩小是 8 的倍数,也就是说存储 13 字节⼤大⼩小的对象,需要找 block ⼤大⼩小为 16 的 pool 获
取空闲块。所有这些都⽤用头信息和链表管理起来,以便快速查找空闲区域进⾏行分配。
⼤大于 256 字节的对象,直接⽤用 malloc 在堆上分配内存。程序运⾏行中的绝⼤大多数对象都⼩小于这个阈
值,因此内存池策略可有效提升性能。
当所有 arena 的总容量超出限制 (64MB) 时,就不再请求新的 arena 内存。⽽而是如同 "⼤大对象" ⼀一
样,直接在堆上为对象分配内存。另外,完全空闲的 arena 会被释放,其内存交还给操作系统。
引⽤用传递
对象总是按引⽤用传递,简单点说就是通过复制指针来实现多个名字指向同⼀一对象。因为 arena 也是
在堆上分配的,所以⽆无论何种类型何种⼤大⼩小的对象,都存储在堆上。Python 没有值类型和引⽤用类
型⼀一说,就算是最简单的整数也是拥有标准头的完整对象。
>>> a = object()
>>> b = a
>>> a is b
True
>>> hex(id(a)), hex(id(b))? ? ? # 地址相同,意味着对象是同⼀一个。
('0x10b1f5640', '0x10b1f5640')
>>> def test(x):? ? ? ?
... print hex(id(x))? ? ?
>>> test(a)
0x10b1f5640? ? ? ? ? # 地址依旧相同。
如果不希望对象被修改,就需使⽤用不可变类型,或对象复制品。
不可变类型:int, long, str, tuple, frozenset
除了某些类型⾃自带的 copy ⽅方法外,还可以:
• 使⽤用标准库的 copy 模块进⾏行深度复制。
• 序列化对象,如 pickle、cPickle、marshal。
下⾯面的测试建议不要⽤用数字等不可变对象,因为其内部的缓存和复⽤用机制可能会造成干扰。
>>> import copy
>>> x = object()
>>> l = [x]? ? ? ? # 创建⼀一个列表。
15
>>> l2 = copy.copy(l)? ? # 浅复制,仅复制对象⾃自⾝身,⽽而不会递归复制其成员。
>>> l2 is l? ? ? ? # 可以看到复制列表的元素依然是原对象。
False
>>> l2[0] is x
True
>>> l3 = copy.deepcopy(l)?? # 深度复制,会递归复制所有深度成员。
>>> l3 is l? ? ? ? # 列表元素也被复制了。
False
>>> l3[0] is x
False
循环引⽤用会影响 deepcopy 函数的运作,建议查阅官⽅方标准库⽂文档。
引⽤用计数
Python 默认采⽤用引⽤用计数来管理对象的内存回收。当引⽤用计数为 0 时,将⽴立即回收该对象内存,
要么将对应的 block 块标记为空闲,要么返还给操作系统。
为观察回收⾏行为,我们⽤用 __del__ 监控对象释放。
>>> class User(object):
... def __del__(self):
... print "Will be dead!"
>>> a = User()
>>> b = a
>>> import sys
>>> sys.getrefcount(a)
3
>>> del a? ? ? ? # 删除引⽤用,计数减⼩小。
>>> sys.getrefcount(b)
2
>>> del b? ? ? ? # 删除最后⼀一个引⽤用,计数器为 0,对象被回收。
Will be dead!
某些内置类型,⽐比如⼩小整数,因为缓存的缘故,计数永远不会为 0,直到进程结束才由虚拟机清理
函数释放。
除了直接引⽤用外,Python 还⽀支持弱引⽤用。允许在不增加引⽤用计数,不妨碍对象回收的情况下间接
引⽤用对象。但不是所有类型都⽀支持弱引⽤用,⽐比如 list、dict ,弱引⽤用会引发异常。
16
改⽤用弱引⽤用回调监控对象回收。
>>> import sys, weakref
>>> class User(object): pass
>>> def callback(r):? ? ? # 回调函数会在原对象被回收时调⽤用。
... print "weakref object:", r
... print "target object dead!"
>>> a = User()
>>> r = weakref.ref(a, callback)?? # 创建弱引⽤用对象。
>>> sys.getrefcount(a)? ? ? # 可以看到弱引⽤用没有导致⺫⽬目标对象引⽤用计数增加。
2? ? ? ? ? ? # 计数 2 是因为 getrefcount 形参造成的。
>>> r() is a?? ? ? ? # 透过弱引⽤用可以访问原对象。
True
>>> del a? ? ? ? ? # 原对象回收,callback 被调⽤用。
weakref object: <weakref at 0x10f99a368; dead>
target object dead!
>>> hex(id(r))? ? ? ? # 通过对⽐比,可以看到 callback 参数是弱引⽤用对象。
'0x10f99a368'? ? ? ? ? # 因为原对象已经死亡。
>>> r() is None? ? ? ? # 此时弱引⽤用只能返回 None。也可以此判断原对象死亡。
True
引⽤用计数是⼀一种简单直接,并且⼗十分⾼高效的内存回收⽅方式。⼤大多数时候它都能很好地⼯工作,除了循
环引⽤用造成计数故障。简单明显的循环引⽤用,可以⽤用弱引⽤用打破循环关系。但在实际开发中,循环
引⽤用的形成往往很复杂,可能由 n 个对象间接形成⼀一个⼤大的循环体,此时只有靠 GC 去回收了。
垃圾回收
事实上,Python 拥有两套垃圾回收机制。除了引⽤用计数,还有个专⻔门处理循环引⽤用的 GC。通常我
们提到垃圾回收时,都是指这个 "Reference Cycle Garbage Collection"。
能引发循环引⽤用问题的,都是那种容器类对象,⽐比如 list、set、object 等。对于这类对象,虚拟
机在为其分配内存时,会额外添加⽤用于追踪的 PyGC_Head。这些对象被添加到特殊链表⾥里,以便
GC 进⾏行管理。
typedef union _gc_head {
struct {
union _gc_head *gc_next;
17
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy;
} PyGC_Head;
当然,这并不表⽰示此类对象⾮非得 GC 才能回收。如果不存在循环引⽤用,⾃自然是积极性更⾼高的引⽤用计
数机制抢先给处理掉。也就是说,只要不存在循环引⽤用,理论上可以禁⽤用 GC。当执⾏行某些密集运
算时,临时关掉 GC 有助于提升性能。
>>> import gc
>>> class User(object):
... def __del__(self):
... print hex(id(self)), "will be dead!"
>>> gc.disable()? ? ? ? # 关掉 GC
>>> a = User()? ? ?
>>> del a? ? ? ? ? # 对象正常回收,引⽤用计数不会依赖 GC。
0x10fddf590 will be dead!
同 .NET、JAVA ⼀一样,Python GC 同样将要回收的对象分成 3 级代龄。GEN0 管理新近加⼊入的年
轻对象,GEN1 则是在上次回收后依然存活的对象,剩下 GEN2 存储的都是⽣生命周期极⻓长的家伙。
每级代龄都有⼀一个最⼤大容量阈值,每次 GEN0 对象数量超出阈值时,都将引发垃圾回收操作。
#define NUM_GENERATIONS 3
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0},
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0},
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0},
};
GC ⾸首先检查 GEN2,如阈值被突破,那么合并 GEN2、GEN1、GEN0 ⼏几个追踪链表。如果没有超
出,则检查 GEN1。GC 将存活的对象提升代龄,⽽而那些可回收对象则被打破循环引⽤用,放到专⻔门
的列表等待回收。
>>> gc.get_threshold()? ? # 获取各级代龄阈值
(700, 10, 10)
>>> gc.get_count()? ? ? # 各级代龄链表跟踪的对象数量
(203, 0, 5)
包含 __del__ ⽅方法的循环引⽤用对象,永远不会被 GC 回收,直⾄至进程终⽌止。
18
这回不能偷懒⽤用 __del__ 监控对象回收了,改⽤用 weakref。因 IPython 对 GC 存在干扰,下⾯面的测
试代码建议在原⽣生 shell 中进⾏行。
>>> import gc, weakref
>>> class User(object): pass
>>> def callback(r): print r, "dead"
>>> gc.disable()? ? ? ? ? # 停掉 GC,看看引⽤用计数的能⼒力。
>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a? ? ? ? # 形成循环引⽤用关系。
>>> del a; del b? ? ? ? ? # 删除名字引⽤用。
>>> wa(), wb()? ? ? ? ? # 显然,计数机制对循环引⽤用⽆无效。
(<__main__.User object at 0x1045f4f50>, <__main__.User object at 0x1045f4f90>)
>>> gc.enable()? ? ? ? ? # 开启 GC。
>>> gc.isenabled()? ? ? ? ? # 可以⽤用 isenabled 确认。
True
>>> gc.collect()? ? ? ? ? # 因为没有达到阈值,我们⼿手⼯工启动回收。
<weakref at 0x1045a8cb0; dead> dead? ? # GC 的确有对付基友的能⼒力。? ?
<weakref at 0x1045a8db8; dead> dead? ? # 这个地址是弱引⽤用对象的,别犯糊涂。
⼀一旦有了 __del__,GC 就拿循环引⽤用没办法了。
>>> import gc, weakref
>>> class User(object):
... def __del__(self): pass? ? ? ? # 难道连空的 __del__ 也不⾏行?
>>> def callback(r): print r, "dead!"
>>> gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK)? # 输出更详细的回收状态信息。
>>> gc.isenabled()? ? ? ? ? ? # 确保 GC 在⼯工作。
True
>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a
>>> del a; del b
>>> gc.collect()? ? ? ? ? ? # 从输出信息看,回收失败。
gc: collecting generation 2...
19
gc: objects in each generation: 520 3190 0
gc: uncollectable <User 0x10fd51fd0>? ? ? # a
gc: uncollectable <User 0x10fd57050>? ? ? # b
gc: uncollectable <dict 0x7f990ac88280>? ? ? # a.__dict__
gc: uncollectable <dict 0x7f990ac88940>? ? ? # b.__dict__
gc: done, 4 unreachable, 4 uncollectable, 0.0014s elapsed.
4
>>> xa = wa()
>>> xa, hex(id(xa.__dict__))
<__main__.User object at 0x10fd51fd0>, '0x7f990ac88280',
>>> xb = wb()
>>> xb, hex(id(xb.__dict__))
<__main__.User object at 0x10fd57050>, '0x7f990ac88940'
关于⽤用不⽤用 __del__ 的争论很多。⼤大多数⼈人的结论是坚决抵制,诸多 "⽜牛⼈人" 也是这样教导新⼿手的。
可毕竟 __del__ 承担了析构函数的⾓角⾊色,某些时候还是有其特定的作⽤用的。⽤用弱引⽤用回调会造成逻
辑分离,不便于维护。对于⼀一些简单的脚本,我们还是能保证避免循环引⽤用的,那不妨试试。就像
前⾯面例⼦子中⽤用来监测对象回收,就很⽅方便。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值