带你了解python垃圾回收机制

python垃圾回收机制

基于C语言源码底层,了解垃圾回收机制的的实现。

  • 引用计数器
  • 标记清楚
  • 分代回收
  • 缓存机制
  • python的c源码

总的就一句话:引用计数器为主、分代码回收和标记清除为辅

1. 引用计数器

1.1 环状双向链表 refchain

在这里插入图片描述

在python程序中创建的任何对象都会放在 refchain 链表中,也就是说他保存者所有创建的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMbFhqAL-1603092836171)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201019151120598.png)]

name = "武沛齐""
age = 18
hobby =["篮球""美女"]
内部会创建一些鼓据【上一个对象、下一个对象、类型、引用个数】
name = "小志"
new = name

内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、val=18]
age = 18

内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、items=元素、元素个数】
hobby =["篮球""美女"]

在C源码中如何体现每个对象中都有的相同的值:PyObject结构体(4个值)。
有多个元素组成的对象: PyObject结构体(4个值) + 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 引用计数器

refchain中的所有对象内部都有一个ob_refcnt用来保存当前对象的引用计数器,顾名思义就是自己被引用的次数,例如:

v1 = 3.14
v2 = 888
v3 = (1,2,3)
v4 = v3

# 3.14, 888, (1,2,3)这几个值得引用计数器分别为:1, 1, 2

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

在C源码中有两个关键的结构体: PyObjectPyVarObject

每个对象中有ob_refcnt就是引用计数器,值默认为1,当有其他变量引用对象时,引用计数器就会发生变化。

  • 引用

    a = 888  # 此时a 的计数为 1
    b = a    # b引用了a, a 的计数 +1,。
    
  • 删除引用

    a = 8888
    b = a
    del b # 删除b变量:b对应对象引用计数器-1
    del a # 删除a变量:a对应对象引用计数器 -1
    
    # 当一个对象的引用计数器为0时, 以为这没有人在使用该对象,此时这个对象就被当做垃圾,是垃圾就要被处理,此时就要进行垃圾回收,避免浪费空间。
    # 垃圾回收:1.对象从refchain链表中删除;2.讲对象销毁,内存归还。(此时就当做引用计数器为0对象就被销毁,会面学习缓存机制是,内部还有缓冲,并不会立即销毁。)
    

实例:

# 创建对象并初始化引用计数器为1
hero = "xiaozhi"
a = hero  # 计数器+1
b = hero  # +1
c = hero  # +1  至此为4

# 再创建一个对象并初始化引用计数器为1
nb = "python"

# 创建的对象全部放在refchain链表中。如下图:

在这里插入图片描述

1.4 循环引用问题

现象实例

v1 = [11,22,33]  # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
v2 = [44,55,66]  #refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
v1.append(v2)    #把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为2.
v2.append(v1)	 #把v1追加到v2中,则v1对应的[11,22,33]对象的引用计数器加1,最终为2.

del v1  #引用计数器-1
del v2  #引用计数器-1

在这里插入图片描述

2. 标记清除

目的:为了解决引用计数器循环计数的不足

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

在这里插入图片描述

在Python内部某种情况下触发,回去扫描可能存在循环应用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器-1;如果是0则垃圾回收.|

此时又有两个问题:

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

3. 分代回收

对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。

// 分代的C源码
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head,                                    threshold,    count */
    {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)},   700,        0}, // 0{{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)},   10,         0}, // 1{{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)},   10,         0}, // 2};

在这里插入图片描述

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

特别注意:0代和1、2代的threshold和count表示的意义不同。

  • 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查。
  • 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。
  • 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。

4. 小结

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

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

  • refchain
  • 2代:10次
  • 1代:10次
  • 0代:700个

在源码内部当个达到各自阈值时,就会触发扫描链表进行标记清除的动作。

5. 缓存

上面大概了解了垃圾回收机制的过程,就是当对象的引用计数器为0 时,就会被当做垃圾回收并释放内存。但实际上并不完全是这样,因为一些对象需要反复创建和销毁,会导致程序执行效率变低,so,为了避免重复创建和销毁一些常见的对象,python便有了引入了缓存机制

例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。

# 启动解释器是,python内部帮我们创建,一些常见得到对象,例如:-100....1024
v1 = 24   # 内部不会开辟内存空间来存储这些值,直接就去池中去取
v2 = 128  # 池中去值
v3 = 100  # 池中去值

  • float类型,维护的free_list链表最多可缓存100个float对象。
  v1 = 3.14    # 开辟内存来存储float对象,并将对象添加到refchain链表。  
  print( id(v1) ) # 内存地址:4436033488  
  del v1    # 引用计数器-1,如果为0则在rechain链表中移除,不销毁对象,而是将对象添加到float的free_list.  
  v2 = 9.999    # 优先去free_list中获取对象,并重置为9.999,如果free_list为空才重新开辟内存。  
  print( id(v2) ) # 内存地址:4436033488  # 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
  • int类型,不是基于free_list,而是维护一个small_ints链表保存常见数据(小数据池),小数据池范围:-5 <= value < 257。即:重复使用这个范围的整数时,不会重新开辟内存。

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

  • list类型,维护的free_list数组最多可缓存80个list对象。

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

  • dict类型,维护的free_list数组最多可缓存80个dict对象。

基于C语言底层实现在这里就不多阐述了,毕竟我还没参透。。。

下面有详细介绍:

C语言底层实现详细过程请看:https://pythonav.com/wiki/detail/6/88/

本次内容的学习视频:https://www.bilibili.com/video/BV1dp4y1C7ja

(完 !)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值