引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制


引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
变量与对象Python 作为一种动态类型的语言 , 其对象和引用分离 。 在 Python 中万物皆对象 , 因此Python 的存储问题等同于对象的存储问题 , 对于每个对象 , Python 都会分配一块内存空间去存储它。
我们通过一个简单的赋值语句来理解 变量与对象 , 如下:
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
变量(testops) , 通过变量指针引用对象 , 变量指针指向具体对象的内存空间 , 取对象的值 。 对象 (9527) , 类型已知 , 每个对象都包含一个头部信息(类型标识符和引用计数器) 。 Python中变量名没有类型 , 类型属于对象 , 对象的类型决定了变量的类型 。
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
如上 , 整数 9527 为一个对象 ,test 是一个变量 。 利用赋值语句 , 引用 test 指向对象 9527。 9527 对象存储在内存中 , 我们可以通过 id 函数 , 查看对象的内存地址 , 引用示意图如下:
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
对于整数和短小的字符等 , 会触发Python的缓存机制 , 即Python将这些对象进行缓存 , 不会为相同的对象分配不同的内存空间 , 如下:
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
如上 , 我们使用 is 关键字判断两个引用所指的对象是否相同 。 可以看到 , 由于Python缓存了小整数(其实也缓存了短字符串 , Python2) , 因此每个对象只存有一份 , 比如 , 使用赋值语句创建小整数 , 如 27 , 并没有创建出新的对象 , 而是创建了一个引用 。 而当使用赋值语句创建大的整数可以有多个相同的对象 , 如使用赋值语句创建大整数 27000 , 此时创建出多个对象 。
引用计数
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
【引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制】在Python中 , 每个对象都有指向该对象的引用总数 , 即引用计数(reference count) 。 一个对象会记录着引用自己的对象的个数 , 每增加1个引用 , 个数加1 , 每减少1个引用 , 个数减1 。 在垃圾回收过程中 , 利用引用计数器方法 , 在检测到对象引用个数为 0 时 , 对普通的对象进行释放内存的机制 。
我们可以使用 sys.getrefcount 方法 , 来查看每个对象的引用计数 。 需要注意的是 , 当使用某个引用作为参数 , 传递给 getrefcount方法时 , 参数实际上创建了一个临时的引用 。 因此 , getrefcount 方法所得到的结果比期望的多1 。
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
由上可见 , l 中的 [t,27] 两个元素 , 都指向了同一个对象 , 实际上 , 容器对象(如 , 列表、字典等)中包含的并不是元素对象本身 , 是指向各个元素对象的引用 。 同时 , l 的引用计数随着 ll 的创建和删除 , 引用计数也随着增加1和减少1 。
导致引用计数增加的场景如下:

  • 对象被创建:t = 27
  • 其它的别名被创建:ll = l
  • 作为参数传递给函数:getrefcount(l)
  • 作为容器对象的一个元素:l = [t, 27]
导致引用计数减少的场景如下:
  • 对象的别名被显式的销毁:del ll
  • 对象的一个别名被赋值给其他对象:l = 789
  • 对象所在的容器被销毁或从容器中删除对象 如 , del ll 或 l.remove(t) 。
  • 一个本地引用离开了它的作用域 , 比如上面的 getrefcount(x) 函数结束时 , x指向的对象引用减1 。
引用计数中的循环引用
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
循环引用即对象之间进行相互引用 , 出现循环引用后 , 利用上述引用计数机制无法对循环引用中的对象进行释放空间 , 从而导致内存泄漏 , 这就是循环引用问题 , 如下:
引用计数|面试中的高频问题,如何理解Python内存管理中的垃圾回收机制文章插图
对象 test 中的元素引用 ops , 而对象 ops 中元素同时来引用 test, 从而造成仅仅删除 test和 ops对象 , 无法释放其内存空间 , 因为他们依然在被引用(引用个数不为0) 。 进一步解释就是循环引用后 , test 和 ops 被引用个数为2 , 删除 test 和 ops 对象后 , 两者被引用的个数变为1 , 并不是0 , 而Python只有在检查到一个对象的被引用个数为0时 , 才会自动释放其内存 , 所以这里无法释放 test 和 ops 的内存空间 , 因此这也是导致内存泄漏的原因之一 。