Python垃圾回收机制

概述

Python的垃圾回收机制采用以引用计数为主,以标记-清除分代收集为辅的策略

引用计数

原理

每个对象有一个整型的引用计数属性ob_refcnt,用于记录对象被引用的次数。每当新的引用指向该对象时,它的引用计数ob_refcnt加一;每当该对象的引用失效时,
他的引用计数ob_refcnt减一。一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。

可以使用sys.getrefcount函数检查该对象的引用计数。(PS: 该函数获取的引用计数总是多1, 因为该函数在调用时也有对该对象的引用)

计数增减条件

引数加一条件

  • 对象被创建
  • 对象被引用
  • 对象作为函数的参数
  • 对象作为容器的元素

引数减一条件

  • 对象被显式销毁。例如: del y
  • 变量重新赋予新的对象
  • 对象离开它的作用域,常见场景: 变量作为函数的参数。函数执行完成后该对象引用计数减一
  • 对象所在的容器被销毁,或从容器中删除对象

优缺点

优点

  • 高效、逻辑简单,只需根据规则对计数器做加减法
  • 实时性。一旦对象的计数器为零,就说明对象永远不可能再被用到,无须等待特定时机,直接释放内存。

缺点

  • 需要为对象分配引用计数空间,增大了内存消耗
  • 嵌套对象需要循环调用,耗时长。如字典对象
  • 循环引用 => 计数器永不为0, 对象无法回收,导致内存泄漏

标记-清除

算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法

一旦无法访问这些对象,要正确处理这些对象,首先需要识别它们。在识别循环的函数内部,维护了两个双向链表:一个列表包含所有要扫描的对象,
另一个将包含所有“暂时”无法访问的对象

GC启动时,它要扫描的所有容器对象都在第一个链表上。目标是移动所有无法到达的对象。由于大多数对象都是可达的,因此移动不可达的对象会更有效,因为这涉及更少的指针更新

标记阶段

对象之间通过指针连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象[全局变量、调用栈、寄存器]出发,沿着有向边遍历对象,
可达的对象标记为活动对象, 不可达的对象就是要被清除的非活动对象。

清除阶段

遍历所有对象,如果发现某个对象没有标记为“可达”,则就回收。

分代收集

分代回收是建立在标记清除技术基础之上

为了限制每次垃圾收集所花费的时间,GC使用了一种流行的优化:generations。这个概念背后的主要思想是假设大多数对象的生命周期都很短,
因此可以在创建后不久就被收集起来。这已被证明非常接近许多 Python 程序的实际情况,因为许多临时对象的创建和销毁速度非常快。
对象越旧,它变得无法访问的可能性就越小。

Python对象被分为三种世代:

  • G0: 刚创建的对象
  • G1: 如果在一轮GC扫描中存活下来,则移至G1,处于G1的对象被扫描次数会减少
  • G2: 如果再次在扫描中活下来,则进入G2,处于G2的对象被扫描次数将会更少

参考文档