Python垃圾回收

Python垃圾回收

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

  • 引用计数器
  • 标记清楚
  • 分代回收
  • 缓存机制
  • Python的C源码(3.8.2版本)

1. 引用计数器

1.1 环状双向链表 refchain

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

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 引用计数器

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

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

在C源码中有两个关键的结构体:PyObject、PyVarObject。

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

  • 引用

    a = 99999
    b = a
    
  • 删除引用

    a = 99999
    b = a
    del b # b变量删除;b对应对象引用计数器-1
    del a # a变量删除;a对应对象引用计数器-1
    
    # 当一个对象的引用计数器为0时,意味着没有人再使用这个对象了,这个对象就是垃圾,垃圾回收。
    # 回收:1.对象从refchain链表移除;2.将对象销毁,内存归还。
    

1.4 循环引用问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y6p1lKhu-1689337573927)(assets/image-20200426125005070.png)]

2.标记清除

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LK0Oknej-1689337573928)(assets/image-20200426125343267.png)]

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

问题:

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

3.分代回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MXi693g-1689337573928)(assets/image-20200426125918008.png)]

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

  • 0代:0代中对象个数达到700个扫描一次。
  • 1代:0代扫描10次,则1代扫描一次。
  • 2代:1代扫描10次,则2代扫描一次。

4.小结

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

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

  • refchain
  • 2代,10寸
  • 1代,10次
  • 0代,700个

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

But,源码内部在上述的流程中提出了优化机制。

5. Python缓存

5.1 池(int)

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

# 启动解释器时,Python内部帮我们创建:-5、-4、..... 257
v1 = 7  # 内部不会开辟内存,直接去池中获取
v2 = 9  # 内部不会开辟内存,直接去池中获取
v3 = 9  # 内部不会开辟内存,直接去池中获取

print(id(v2),id(v3))

v4 = 999
v5 = 666
v6 = 666

5.2 free_list(float/list/tuple/dict)

当一个对象的引用计数器为0时,按理说应该回收,内部不会直接回收,而是将对象添加到 free_list 链表中当缓存。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list。

v1 = 3.14  # 开辟内存,内部存储结构体中定义那几个值,并存到refchain中。

del v1     # refchain中移除,将对象添加到 free_list 中(80个),free_list满了则销毁。


v9 = 999.99 # 不会重新开辟内存,去free_list中获取对象,对象内部数据初始化,再放到refchain中。

详见:https://pythonav.com/wiki/detail/6/88/

总结:

# python采用的是以引用计数为主,以分代回收和标记清除为辅的垃圾回收机制

# 1 引用计数
"""
在python中,每创建一个对象,那么python解释器会自动为其设置一个特殊的变量,这个变量称为引用计数(初始值默认是1)。一旦有一个新变量指向这个对象,那么这个引用计数的值就会加1。如果引用计数的值为0。那么python解释器的内存管理系统就会自动回收这个对象所占的内存空间,删除掉这个对象。
引用计数+1的情况:
    对象被创建,例如a = "yuan"
    对象被引用,例如b = a
    对象被作为参数,传入到一个函数中,例如fun(a)
    对象作为一个元素,存储在容器中,例如data_list=[a,b]
引用计数-1的情况:
    对象的别名被显式销毁,例如del a
    对象的别名被赋予新的对象,例如a = 24
    一个对象离开它的作用域,例如func函数执行完毕时,func函数中的局部变量(全局变量不会)
    对象所在的容器被销毁,或从容器中删除对象
"""
# 2 分代回收
"""
既然已经有引用计数了,那么为什么还要提出分代回收呢?原因就是引用计数没办法解决“循环引用”的情况。
a = ["yuan", ]   # 语句1
b = ["rain", ]    # 语句2
a.append(b)          # 语句3
b.append(a)          # 语句4
# 此时对象的值:a = ["yuan", b]   b = ["rain", a]
del a                # 语句5
del b                # 语句6
# 执行完语句5和语句6是希望同时删除掉a对象和b对象

在执行"del a"语句之后,只是删除了对象的引用,也就是此时a变量这个名字被删除,也就是此时对象["yuan",b]的引用计数减1;执行"del b"语句也是同样的情况。但是,此时,由于显式指向它们的变量已经不存在了,所以也没办法删除了,就会导致它们一直存在于内存空间中。 这就是循环引用出现的问题。 此时,单靠引用计数没办法解决问题。所以便提出了分代回收

注意:在分代回收中,如果某对象的引用计数为0,那么它所占的内存空间同样也会被python解释器回收。

a、此时在python中每创建一个对象,那么就会把对象添加到一个特殊的“链表”中,这个链表称为"零代链表"。每当创建一个新的对象,那么就会将其添加到零代链表中。当这个"零代链表"中的对象个数达到某一个指定的阀值的时候,python解释器就会对这个"零代链表"进行一次“扫描操作”。这个“扫描操作”所做的工作是查找链表中是否存在循环引用的对象,如果在扫描过程中,发现有互相引用的对象,那么会让这些对象的引用计数都减少1。此时,如果某些对象引用计数变成0,那么就会被python解释器回收其所占用的内存空间;如果对象的引用计数仍然不为0,那么会把此时存活的对象迁移到“一代链表”中。

b、同样,python解释器也会在一定的情况下,也扫描“一代链表”,判断其中是否存在互相引用的对象。如果存在,那么同样也是让这些对象的引用计数都减少1。此时,如果某些对象引用计数变成0,那么就会被python解释器回收其所占用的内存空间;如果对象的引用计数仍然不为0,那么会把此时存活的对象迁移到“二代链表”中。

c、同样,python解释器也会在一定的情况下,也会扫描"二代链表",判断其中是否存在互相引用的对象。如果存在,那么同样也是让这些对象的引用计数都减少1。此时,如果某些对象引用计数变成0,那么就会被python解释器回收其所占用的内存空间;如果对象的引用计数仍然不为0,那么会把此时存活的对象迁移到一个新的特殊的内存空间。此时重新进行"零代链表 -> 一代链表 -> 二代链表"的循环。

这就是python的分代回收机制。
"""

# 标记清除
"""
那么既然已经有分代回收了,那么为什么又要提出标记-清除呢?
原因就是分代回收没办法解决“误删”的情况。
a = ["yuan", ]   # 语句1
b = ["rain", ]    # 语句2
a.append(b)          # 语句3  
b.append(a)          # 语句4  
# 此时对象的值:a = ["yuan", b]   b = ["rain", a].   ["yuan", b] 、["rain", a]的引用计数都为2
del a                # 语句5
# 此时["yuan", b]的引用计数为1, ["rain", a]的引用计数为2
# 执行完语句5只希望删除a对象,保留b对象

如果按照分代回收的方式来处理上述语句。那么,python解释器在执行完语句5之后。在一定的情况下进行查找循环引用对象的时候,会发现此时["rain", a]对象和["yuan", b]对象存在互相引用的情况。所以此时就会让这两个对象的引用计数减1。此时,["yuan", b]对象的引用计数为0,所以["yuan", b]对象被真正删除,但是其实此时["rain", a]对象中是有一个变量引用原来的["yuan", b]对象的。如果["yuan", b]对象被真正删除的话,那么此时时["rain", a]对象中的a变量就没有用了,就没有办法访问了。但是其实我们是希望它有用的,所以这个时候就出现“误删”的情况了。所以此时就需要结合“标记-清除”来解决问题了。

标记-清除:
此时同样是检测链表中的相互引用的对象,然后让它们的引用计数减1之后;
但是此时会将所有的对象分为两组:死亡组(death_group)和存活组(survival_group),把引用计数为0的对象添加进死亡组,其它的对象添加进存活组;
此时会对存活组的对象进行分析,只要对象存活,那么其内部的对象当然也必须存活。如果发现内部对象死亡,那么就会想方设法让其活过来,通过这样子就能保证不会删错对象了。

题目的重新分析:
  在检查死亡组的时候,会发现["rain", a]对象中的a所指向的对象存在于死亡组中,所以就会想方设法让其复活,此时就能够保证["rain", a]对象中所有的对象都是存活的。

"""

更多详细请参考:https://pythonav.com/wiki/detail/6/88/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值