1. 垃圾回收机制的算法分类
python垃圾回收算法通常有三类:引用计数,标记清除和分代回收,主要以引用计数为主,标记清除和分代回收为辅
2. 对象的存储方式——refchain环状双向链表
在Python中创建的任何对象都会放在refchain的双向链表中
C语言中结构体的定义代码,即refchain中存在四个共性的四个属性值:prev、next、refcnt(引用计数器)、type(类型)
但是如列表、元组、字典等类型时,增加size属性,即元素个数
不同的类型的不同属性
3. 引用计数
1. 引用计数的原理
1. 每个对象在创建时都会有一个引用计数器来记录引用次数,即ob_refcnt属性,默认值为1
2. 当对象被引用时,引用计数器会进行+1
3. 当对象不被引用或者销毁时,引用计数器会进行-1
4. 当引用计数器为0时,对象被回收,即在refchain链表中删除,释放系统内存
例:
import sys
class A:
def __init__(self):
print('对象被创建')
def __del__(self):
print('对象被销毁')
# sys.getrefcount方法用来获取指定对象计数器的值
print(sys.getrefcount(A())) # 创建A()对象,计数器值为 1
a = A()
print(sys.getrefcount(a)) # 对象A()创建并被变量a引用,计数器值为 2
b = a
print(sys.getrefcount(b)) # 变量b等于变量a,即指向a的地址A(), 计数器为 3
a = 1
print(sys.getrefcount(b)) # 变量a指向其他对象,A()对象的引用数 -1,变为 2
b = 1 # 变量b指向其他对象,A()对象的引用数 1,同时A()对象被销毁,计数器 -1,变为 0,被回收
print('程序结束')
2. 引用计数的优缺点
1. 优点
1. 简单高效
2. 时效性高,只要当对象的计数器为0时,立即进行回收
2. 缺点
1. 维护计数器消耗大量的资源
2. 当出现字典,列表对对象的循环套用引用时,会造成对象之间互相引用,引发内存泄漏
4. 标记清除
1. 循环引用造成的后果
# 循环引用例子
class L1(dict):
def __del__(self):
print('对象1被销毁')
class L2(dict):
def __del__(self):
print('对象2被销毁')
l1 = L1() # 对象L1创建的时候计数器为 1,,被l1引用,此时计数器为 2
l2 = L2() # 对象L2创建的时候计数器为 1,,被l1引用,此时计数器为 2
print(sys.getrefcount(l1)) # 2
print(sys.getrefcount(l2)) # 2
l1['a'] = l2 # l1的键a引用l2,此时L2对象的引用为2,所以计数器为 3
l2['b'] = l1 # l2的键b引用l1,此时L1对象的引用为2,所以计数器为 3
print(sys.getrefcount(l1)) # 3
print(sys.getrefcount(l2)) # 3
# 字典被删除,但是字典中的值(即L1,L2对象)互相引用着,理论上用不着,但是无法被引用计数算法回收
del l1 # 删除l1,但是这里对象仍在被引用,计数器仍为 2,不会被回收
del l2 # 删除l2,但是这里对象仍在被引用,计数器仍为 2,不会被回收
print('程序结束') # 对象在整个程序结束才被回收,而不是del的时候被回收
图解:
2. 标记清除的实现原理
1. 标记:将所有的对象看做是一个点,并将对象的引用关系构造图结构,从根节点出发遍历所有的点,能访问到的点标记为“可达对象”
2. 清除:遍历所有对象,若没有被标记为“可达对象“则进行回收
class A:
def __init__(self):
pass
def __del__(self):
print('对象被销毁')
def func():
a = A() # a引用对象A
b = A() # b引用对象A
c = A() # c引用对象A
d = A() # d引用对象A
e = c # e引用c
f = c # f引用c
# a,b对象的属性互相引用
a.obj = b
b.obj = a
# 不返回a,b,表明两个变量引用被删除了
return [d,e,f]
g = func()
print('程序结束,回收垃圾')
5. 分代回收
分代回收是建立在标记清除的基础上,扫描对象需要定义一个触发点,如果时时刻刻进行扫描,那么增加了程序的运行时间,通常将循环引用的对象分为三代:0代、1代和2代
0代:对象刚刚被创建时分配到0代链表
1代:经过一轮GC扫描存货下来的,放置1代链表中
2代:再次经过扫描存活下来的对象,分至2代链表中
触发GC扫描机制