python面试题整理05-内存管理与引用计数/垃圾回收机制
python面试题整理05-内存管理与引用计数/垃圾回收机制

python面试题整理05-内存管理与引用计数/垃圾回收机制

说实话,感觉又是很工程化、偏底层的一章。也是自己平时几乎不怎么考虑的。

首先我们得知道,python运行时候是要占用内存的。一般情况下我们跑简单的程序也不用太担心,毕竟内存都够。但是一旦程序大了,有些地方写得不太对的话,就有概率出现爆内存的问题。

OK,那么就要回头看看,python到底如何管理内存的,以及有哪些坑点,需要我们注意。

Python 的内存管理主要依赖引用计数作为即时回收机制,辅以分代垃圾回收(标记-清除)来解决循环引用问题。

引用计数

每个 Python 对象内部都有一个计数器,记录有多少个“名字”指向它。当一个对象引用计数变为 0,对象立即释放。这是 Python 内存管理的第一优先级机制

下面这个代码就展示了列表对象有多少个引用计数。至于为啥要减去一,主要是因为我们统计引用计数用到的函数也算一次引用计数……

import sys
a = []
print(sys.getrefcount(a)-1)

常见的增加引用计数:

  • 赋值:b = a
  • 作为参数传入函数
  • 放入容器(list / dict / set)

常见的减少引用计数:

  • del a(这儿需要注意,del只是接触绑定,不是直接释放内存)
  • 变量被重新绑定
  • 函数结束(局部变量销毁)
  • 容器删除元素

看起来似乎挺不错的,有用到某个对象就留着,没人用到了就清零。所以引用计数优点就是实时回收、实现简单可预测、内存占用稳定。但是其也有个致命缺点,就是无法解决循环引用。、

循环引用和gc

啥叫循环引用呢?

a = []
b = []
a.append(b)
b.append(a)
del a
del b

就是如上所示,b引用a,a引用b。按照上述逻辑a列表的对象引用计数为2。常规逻辑来说,我们删除a、删除b之后,引用计数应该为0。但是因为这儿是循环引用,有bug,引用计数不会为0了,这就导致引用计数永远不会释放这个对象的内存,即便在程序里已经删除了对应的变量。

这就需要第二个工具,垃圾回收器gc出来工作了。gc专门处理“引用计数不为0,但从程序角度已经不可达的对象。

gc主要通过两步走战略来解决循环引用问题:

  1. 标记(Mark):从根对象出发,标记所有“还能访问到”的对象
  2. 清除(Sweep):清除没有被标记的对象

“根对象”是啥呢?主要包括

  • 全局变量
  • 当前栈帧中的局部变量
  • 活跃线程相关对象

循环引用但不可达的对象,会在这一步被回收。

当然了,gc也不可能像某洲一样全天24h全量扫盘,而是分代垃圾回收(到底是谁翻译的啊……奇奇怪怪的名字)。

分代是什么意思呢?

  • 第 0 代:新创建对象
  • 第 1 代:经历过一次 gc 还存活
  • 第 2 代:长期存活对象

分代垃圾回收基本策略:

  • 年轻代回收频率高
  • 老年代回收频率低

此外,gc也不是啥对象都跟踪,主要是容易触发循环引用的list、dict、set等容易跟踪,其他的int、float、str、tuple啥的就默认不参与gc,这可能也是为什么 tuple 在性能上更友好。

内存泄漏

ok,我们了解了引用计数以及gc,那有没有可能出现这两种机制都不回收的情况呢?有的,兄弟,包有的。下面就是常见的python内存泄漏情况。

全局缓存无限增长

全局变量且cache[x]heavy_obj(x) 建立了强引用,引用计数 ≥ 1,永远不为 0。此外cache 是根对象(全局变量),所有 value 都从 cache 可达。因此不会回收。

    cache = {}
    def f(x):
        cache[x] = heavy_obj(x)
    

    闭包引用大对象

    def outer():
        big = [0] * 10_000_000
        def inner():
            return big
        return inner

    这儿inner 是一个函数对象,inner.__closure__ 保存了对 big 的引用。只要 fn 还存在:永远不会归零,big 的引用计数 ≥ 1。gc那儿来看对象也是正常可达。

    (3)循环引用 + del 方法(高危)

    如果对象定义了 __del__,gc 不会自动回收循环引用对象。这个似乎更少见,del本身好像就没咋见过。有兴趣的朋友可以自己看下。

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注