PancrasL的博客

python垃圾回收

2020-10-15

img

引用计数——reference counting

一个简单样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 用于获取引用计数的函数,输出结果会比实际引用数多1,因为在调用该函数时引用数增加了1
from sys import getrefcount

class Student():
def __init__(self, name, age):
self.name = name
self.age = age


s1 = Student('Tom', 11)
print(getrefcount(s1) - 1) # 引用数为1

s2 = s1
s3 = s1
s4 = s1
print(getrefcount(s1) - 1) # 引用数为1+3=4

输出结果:

1
2
1
4

缺陷:循环引用导致的内存泄漏

  • 内存泄漏

指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。导致程序运行速度减慢甚至系统崩溃等严重后果。

del() 函数的对象间的循环引用是导致内存泄漏的主凶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys

class Student():
def __init__(self, name, age, deskmate = None):
self.name = name
self.age = age
self.deskmate = deskmate # 同桌:引用另一个学生

s1 = Student("Tom", 11)
s2 = Student("Mike", 12)
print(f'循环引用前, s1:{sys.getrefcount(s1)-1},s2:{sys.getrefcount(s2)-1}')

s1.deskmate = s2
s2.deskmate = s1
print(f'循环引用后, s1:{sys.getrefcount(s1)-1},s2:{sys.getrefcount(s2)-1}')

# 理论上del s1 和 del s2后s1,s2的引用次数应该为0,但由于循环引用,删除对象后引用计数仍然为1
# 由于循环引用,del 对象后仍然会使引用计数+1,导致垃圾回收器都不会回收它们,所以就会导致内存泄露
del s1
del s2
# print(f'del对象后, s1:{sys.getrefcount(s1)-1},s2:{sys.getrefcount(s2)-1}')

输出结果:

1
2
3
循环引用前, s1:1,s2:1
循环引用后, s1:2,s2:2
# del对象后, s1:1,s2:1
  • 解决方案
    • 标记清除技术——mark and sweep
    • 分代回收技术——generation collection
    • 手动使用gc模块

标记-清除机制——mark and sweep

基本思想

先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。

算法实现

标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。

img

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

分代技术——generation collection

基本思想

将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。

Reference