Python 垃圾回收机制
Python GC 原理
简介
- 引用计数 (python 默认):记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数 ob_ref 加 1 ,每当该对象的引用失效时计数 ob_ref 减 1,一旦对象的引用计数为 0,该对象立即被回收
- 标记清除:第一段给所有活动对象标记,第二段清除非活动对象
- 分代回收: python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,比如有年轻代、中年代、老年代,年轻代最先被回收
GC 作为现代编程语言的自动内存管理机制,专注于两件事:
- 找到内存中无用的垃圾资源
- 清除这些垃圾并把内存让出来给其他对象使用。
引用计数
Python 语言默认采用的垃圾收集机制是『引用计数法 Reference Counting』,该算法最早 George E.Collins 在 1960 的时候首次提出,50 年后的今天,该算法依然被很多编程语言使用,『引用计数法』的原理是:每个对象维护一个 ob_ref
字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数 ob_ref 加 1,每当该对象的引用失效时计数 ob_ref 减 1,一旦对象的引用计数为 0,该对象立即被回收,对象占用的内存空间将被释放。它的缺点是需要额外的空间维护引用计数,这个问题是其次的,不过最主要的问题是它不能解决对象的“循环引用”,因此,也有很多语言比如 Java 并没有采用该算法做来垃圾的收集机制。
我们先看一个例子
class Person:
def __init__(self, name):
self.name = name
def __del__(self):
print(f'{self.name} 即将被删除')
person = Person('正心')
# print(person)
person_back = person
input('输入任意内容删除 person')
del person
input('输入任意内容结束程序')
# 程序结束的时候会释放所有的内容
# __del__ 是为了理解垃圾回收机制
什么是循环引用 ?A和B相互引用而再没有外部引用 A 与 B 中的任何一个,它们的引用计数虽然都为 1,但显然应该被回收,例子:
a = {} # 对象 A 的引用计数为 1
b = {} # 对象 B 的引用计数为 1
a['b'] = b # B的引用计数增1
b['a'] = a # A的引用计数增1
del a # A的引用减 1,最后A对象的引用为 1
del b # B的引用减 1, 最后B对象的引用为 1
在这个例子中程序执行完 del
语句后,A、B 对象已经没有任何引用指向这两个对象,但是这两个对象各包含一个对方对象的引用,虽然最后两个对象都无法通过其它变量来引用这两个对象了,这对GC来说就是两个非活动对象或者说是垃圾对象,但是他们的引用计数并没有减少到零。因此如果是使用引用计数法来管理这两对象的话,他们并不会被回收,它会一直驻留在内存中,就会造成了内存泄漏(内存空间在使用完毕后未释放)。为了解决对象的循环引用问题,Python 引入了标记-清除和分代回收两种 GC 机制。
标记清除
『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。 它分为两个阶段:
- 第一阶段是标记阶段,GC会把所有的『活动对象』打上标记
- 第二阶段是把那些没有标记的对象『非活动对象』进行回收。
那么 GC 又是如何判断哪些是活动对象哪些是非活动对象的呢 ?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。 根对象就是全局变量、调用栈、寄存器。
在上图中,我们把小黑圈视为全局变量,也就是把它作为 root object,从小黑圈出发,对象 1 可直达,那么它将被标记,对象 2、3 可间接到达也会被标记,而 4 和 5 不可达,那么 1、2、3 就是活动对象,4 和 5 是非活动对象会被 GC 回收。
标记清除算法作为 Python 的辅助垃圾收集技术主要处理的是一些容器对象,比如 list、dict、tuple,instance 等,因为对于字符串、数值对象是不可能造成循环引用问题。Python 使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
分代回收
分代回收是一种以空间换时间的操作方式,Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了 3 “代”,分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代),他们对应的是 3 个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python 垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为 Python 的辅助垃圾收集技术处理那些容器对象
引用计数和对象销毁
CPython 的实现中,对象会包括一个引用计数器。当对象被赋值给一个变量时,这个计数器会递增;当变量被删除时,这个计数器会递减。当引用计数器的值为0时,表示我们的程序不再需要这个对象并且可以销毁这个对象。对于简单对象,当执行删除对象的操作时会调用 __del__()
方法。
对于包含循环引用的复杂对象,引用计数器有可能永远也不会归零,这样就很难让 __del__()
被调用。
我们用下面的一个类来看看这个过程中到底发生了什么。
class RectAngle:
def __init__(self, width, height):
self.width = width
self.height = height
def __del__(self):
print("被删除 {0}".format(id(self)))
我们可以像下面这样创建和删除这个对象。
>>> p = RectAngle(4, 5)
>>> del p
被删除
2643407508256
我们先创建,然后删除了Noisy
对象,几乎是立刻就看到了__del__()
方法中输出的消息。这也就是说当变量x
被删除后,引用计数器正确地归零了。一旦变量被删除,就没有任何地方引用Noisy
实例,所以它也可以被清除。
下面是浅复制中一种常见的情形。
>>> ln = [RectAngle(4, 5), RectAngle(4, 6)]
>>> ln2= ln.copy()
>>> del ln
>>>
Python没有响应 del
语句。这说明这些 Noisy
对象的引用计数器还没有归零,肯定还有其他地方引用了它们,下面的代码验证了这一点。
>>> del ln2
被删除 2643407508592
被删除 2643407508480
循环引用和垃圾回收
下面是一种常见的循环引用的情形。一个父类包含一个子类的集合,同时集合中的每个子类实例又包含父类的引用。
下面我们用这两个类来看看循环引用。
class Parent:
def __init__(self, *children):
self.children = list(children)
for child in self.children:
child.parent = self
def __del__(self):
print("删除 {} {}".format(self.__class__.__name__, id(self)))
class Child:
def __del__(self):
print("删除 {} {}".format(self.__class__.__name__, id(self)))
一个Parent
的instance
包括一个children
的列表。
每一个Child
的实例都有一个指向Parent
类的引用。当向Parent
内部的集合中插入新的Child
实例时,这个引用就会被创建。
我们故意把这两个类写得比较复杂,所以下面让我们看看当试图删除对象时,会发生什么。
>>> p = Parent(Child(), Child())
>>> id(p)
2643407508984
>>> del p
>>>
Parent
和它的两个初始Child
实例都不能被删除,因为它们之间互相引用。
下面,我们创建一个没有Child
集合的Parent
实例。
>>> p = Parent()
>>> id(p)
2643407509096
>>> del p
删除
Parent
2643407509096
>>>
和我们预期的一样,这个Parent
实例成功地被删除了。
许多基本的特殊方法,它们是我们在设计任何类时的基本特性。这些方法已经包含在每个类中,只是它们的默认行为不一定能满足我们的需求。
我们几乎总是需要重载__repr__()
、__str__()
。这些方法的默认实现不是非常有用。
我们几乎不需要重载__bool__()
方法,除非我们想自定义集合。这是第6章“创建容器和集合”的主题。
我们常常需要重载比较运算符。默认的实现只适合于比较简单不可变对象,但是不适用于比较可变对象。我们不一定要重写所有的比较运算符
另外两个较为特殊的方法__new__()
和__del__()
有更特殊的用途。大多数情况下,使用__new__()
来扩展不可变类型。
基本的特殊方法和__init__()
方法几乎会出现在我们定义的所有类中。其他的特殊方法则有更特殊的用途,它们分为6个不同的类别。
- 属性访问 :这些特殊方法实现的是表达式中
object.attribute
的部分,它通常用在一个赋值语句的左操作数以及del
语句中。 - 可调用对象 :一个实现了将函数作为参数的特殊方法,很像内置的
len()
函数。 - 集合 :这些特殊方法实现了集合的很多特性,包括
sequence[index]
、mapping[index]
和set | set
。 - 数字 :这些特殊方法提供了算术运算符和比较运算符。我们可以用这些方法扩展Python支持的数值类型。
- 上下文 :有两个特殊方法被我们用来实现可以和
with
语句一起使用的上下文管理器。 - 迭代器 :有一些特殊方法定义了一个迭代器。没有必要一定要使用这些方法,因为生成器函数很好地实现了这种特性。但是,我们可以了解如何实现自定义的迭代器。
案例
class Person:
def __init__(self, name):
self.name = name
def __del__(self):
print(f'{self.name} 即将被删除')
arr = []
def create_person():
person = Person('正心')
# 没有删除 person
input('输入任意内容结束函数')
# return person
arr.append(person)
# arg = create_person()
create_person()
del arr
input('输入任意内容结束程序')