Python 内存管理 今天药忘吃喽~ 2022-07-15 02:23 121阅读 0赞 为提升执⾏行性能,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\_\_ 承担了析构函数的⾓角⾊色,某些时候还是有其特定的作⽤用的。⽤用弱引⽤用回调会造成逻 辑分离,不便于维护。对于⼀一些简单的脚本,我们还是能保证避免循环引⽤用的,那不妨试试。就像 前⾯面例⼦子中⽤用来监测对象回收,就很⽅方便。
还没有评论,来说两句吧...