内存回收机制

引用计数器为主

标记清除和分代回收为辅+缓存机制

1. 引用计数器

1.1 环状双向链表refchain

python程序创建的任何对象都会放在rechain链表中

name = "Adsa"
age = 17
hobby = ["ads", "dasads"]

python内部会为这些对象创建一个结构体[上一个对象,下一个对象,类型,引用个数]
比如
age = 17 结构体包括[上一个对象,下一个对象,类型,引用个数, val=17]
hobby = ["ads", "dasads"] 结构体包括[上一个对象,下一个对象,类型,引用个数, items=元素, 个数]
不同类型创建的结构体包含的内容不一样

在C源码中, 每个对象对应结构的都有的值: PyObject结构体[上一个对象,下一个对象,类型,引用个数]

有多个元素组成的对象: PyObject结构+ob_size(表示元素个数)

1.2 类型封装结构体

data = 3.14

内部会创建:
	_ob_next = refchain中上一个对象
	_ob_prev = refchain中下一个对象
	ob_refcnt = 1
	ob_type = float
	ob_fval = 3.14

1.3 引用计数器

v1 = 3.14
v2 = 333
v3 = (1,2,3)

当python程序运行时,会根据数据类型的不同找到其对应的结构体,根据结构体中的字段进行创建相关数据, 然后将对象添加到refchain双向环状链表中.

C源码中有两个关键的结构体:PyObject, PyVarObject.

每个对象对应的结构体中有ob_refcnt就是引用计数器,默认值为1, 当有其它变量引用对象时, 引用计数器会发生变化.

a = 999
b = a
def b # b变量删除, b对应的对象(也是a对应的对象)引用计数器-1
def a # a变量删除, a对应的对象引用计数器-1

# 当引用计数器为0时, 意味着没有人再使用这个对象了, 这个对象就是垃圾, 可进行垃圾回收
# 回收: 1. 对象从refchain链表中移除. 2. 将对象销毁, 内存归还(但是还有缓存机制)

在这里插入图片描述

1.4 循环引用问题

在这里插入图片描述

当v1和v2删除后,理论上v1和v2对应对象的引用计数器应该为0, 但是实际上的引用计数器为1, 它将永远在内存, 为解决循环引用问题, 引入标记清除.

2. 标记清除

目的:为了解决循环引用问题

实现:在python的底层维护一个链表, 链表中专门放那些可能存在循环引用的对象(比如:list/tuple/set)

在这里插入图片描述

在python的内部, 某种情况下, 会去扫描可能存在循环引用的链表中每个元素, 检查是否出现循环引用, 如果有, 则让双方的引用计数器-1, -1后引用计数为额0, 则垃圾回收.

问题:

  • 什么时候扫描?
  • 可能存在循环引用的链表扫描的代价大, 每次扫描耗时久.

3. 分代回收

在这里插入图片描述

将可能存在循环引用的对象维护成3个链表:

0代: 阈值: 0代中对象个数达到700个扫描一次.

1代: 阈值: 0代扫描10次, 则1代扫描一次.

2代: 阈值: 1代扫描10次, 则2代扫描一次.

扫描过程中, 如果是循环引用, 则让双方的引用计数器-1, 当引用计数器值为0, 则是垃圾, 可回收, 不是垃圾则升级放到下一代中, 当0代扫描10次, 则1代扫描一次, 依次类推.

4. 小结

在python中维护了一个refchain的双向环状链表, 这个链表存储了程序中创建的所有对象, 每种类型的对象都有一个ob_refcnt引用计数器的值, 引用个数+1, -1, 当引用计数器变为0时, 则会进行垃圾回收(对象销毁, 从refchain移除).

但是在python中, 对于那些可以有多个元素的对象可能会存在循环引用的问题, 为了解决这个问题, python又引入了标记清楚和分代回收, 在其内部维护了4链表.

  • refchain --存放所有对象, 而下面的3个链表只存放可能发生循环引用的对象

  • 2代 1代扫描10次

  • 1代 0代扫描10次

  • 0代 700个

在源码的内部, 当达到各自的阈值时, 就会触发扫描链表, 进行标记清除操作(有循环引用, 则各自-1)

但是,源码内部, 在上述的流程中提出了优化机制.—缓存

5. 缓存

5.1 池

为了避免重复创建和销毁常见对象, python维护了一个池.

# 在启动解释器时, python内部帮我们创建:-5, -4, ..., 256
v1 = 7	# 内部不会开辟内存, 直接去池中获取		注意: 此时7对象的引用计数器为2
v2 = 9	# 内部不会开辟内存, 直接去池中获取		注意: 此时9对象的引用计数器为2
v3 = 7	# 内部不会开辟内存, 直接去池中获取		注意: 此时7对象的引用计数器为3

print(id(v1), id(v3)) 	# 这里的v1和v3的id应该是一致的
v4 = 666	# 内部会开辟内存, 因为池中没有

5.2 free_list

当一个对象的引用计数器为0时, 按理该对象应该回收, 但是实际上, 内部不会直接回收, 而是将对象添加到free_list链表中缓存. 以后再去创建对象**(相似类型)**时, 不再重新开辟内存, 而是直接使用free_list.

v1 = 3.14	# 开辟内存, 内部会创建对应的结构体, 并加入到refchain中.

def v1		# 从refchain中移除, 将对象添加到free_list中, 当缓冲池满了, 则销毁
	
v2 = 9.99	# 不会再重新开辟内存, 去free_list中获取对象, 进行对象内部初始化, 再放到refchain中

# 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
print(id(v1), id(v2)) 	# 这里的v1和v2的id应该是一致的

注意, free_list是有个数限制的

str类型,维护unicode_latin1[256]链表,内部将所有的ascii字符缓存起来,以后使用时就不再反复创建。

 # 相当于有个字符池
 v1 = "A"
 print( id(v1) ) # 输出:4517720496
 del v1
 v2 = "A"
 print( id(v2) ) # 输出:4517720496
  
 # 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
 v1 = "wupeiqi"
 v2 = "wupeiqi"
 print(id(v1) == id(v2)) # 输出:True

tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。

  v1 = (1,2)
  print( id(v1) )
  del v1  # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
  v2 = ("武沛齐","Alex")  # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
  print( id(v2) )
  v1 = (1,2)
  print( id(v1) )
  del v1  # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
  v2 = ("武沛齐","Alex")  # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
  print( id(v2) )

参考文献: https://pythonav.com/wiki/detail/6/88/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值