Python垃圾回收机制

先记着一句话

引用计数器为主,标记清除和分代回收为辅+缓存机制

1、引用计数器

1.1环状双向链表(refchain)

在这里插入图片描述
要说引用计数,先说一些Python中使用到的双向环状链表refchain。

上图是一个双向链表,所谓双向,意思是在中间的某一个节点,可以找到其下一个节点和上一个节点。在Python中创建的任何一个对象都会加到双向链表中。

例如在Python创建以下三个对象:

name = 'Jack'
age = 18
hobby = ['篮球']

如果代码一执行,会创建三个对象,一个字符串对象,一个整型对象,一个列表对象,Python会把它们放到双向链表中,这也帮助我们维护了Python中所有的对象。

不同的对象放到链表中是否不同呢?是否有不一样的东西呢?在Python中创建的不同类型的对象,在放到双向链表中也会稍微有些不同,不同的地方在于内部存储的数据可能会不一样;它们还有一些相同的点,下面来分析一下相同的地方。

Python在执行name = 'Jack’的时候,内部会创建一个类似于C语言的结构体,或者理解成内部会创建一个大包,会创建一些数据放在这个包里面。那这个包会创建什么数据呢?它会创建它的上一个对象的指针,也就是链表指针,还会创建它的下一个对象的指针。

因为创建的对象最终要加到双向链表中,因此数据包的内部地存储其上一个对象的链表指针和下一个对象的链表指针,这样以便于可以使当前数据可以双向查找其它的数据。

数据包中除了两个链表指针之外。还有数据类型,比如name = 'Jack’就是一个字符串类型,除了类型以外,还有当前数据被引用的个数。

现在解释一下什么是引用个数,比如现在有Python代码:

name = 'Jack'
new = name

这样new也会指向name,这时候Python不会为new重新创建一个结构体放进双向队列中,而是让new也指向’Jack’,这时候在环装链表里面对应的’Jack’结构体内部会存上一个2,也就是引用计数为2,也就是有两个变量用到了这一块数据。

综上,结构体内部会创建以上四个数据,[上一个对象的指针,下一个对象的指针,数据类型,引用计数],这是不同类型对象的相同之处。

除了上面四个相同的数据值之外,不同的对象之间还有不同的地方,比如Python代码age=18,会在结构体内部创建value=18,也就是结构体包括的数据有[上一个对象的指针,下一个对象的指针,数据类型,引用计数,value=18]。对于一个列表对象,其Python代码为hobby = [‘篮球’],因此其结构体的数据类型为[上一个对象的指针,下一个对象的指针,数据类型,引用计数,item=列表元素]。

因此对于Python中的不同对象,存储到双向链表中时创建的结构体有一些值相同,有一些值不相同,对于列表对象,不仅要有item存放数据,还得有一个数据存放列表元素的个数。

综上,我们知道在Python中创建的任何对象都会加到双向链表中,但是每一种类型的对象加到链表中,结构体中存储的数据的个数可能是不一样的。

下面通过源码分析,Python对象是怎么放到双向链表中的。
在这里插入图片描述
以上是对应的C语言源码,在源码中的第9-13行,定义了一个结构体,结构体中定义了三个变量,第一个变量_PyObjects_HRAD_EXTRA,是5-7行宏定义的一个结构体,这个宏定义的结构体中有定义了两个变量。所以第9-13行定义的结构体可以存放四个变量值,分别是对应我们上面讲解的两个链表指针,数据类型,引用计数器。这就是不同对象存放在结构体中相同的部分。

接着看源码中的第15-18行,这里定义了一个结构体,结构体中的第一个变量是第9-13行定义的结构体,也就是当前结构体包含了前面介绍的四个变量[两个链表指针,数据类型,引用计数器]。同时这个结构体中还创建了一个变量ob_size,用于存放元素个数。前面介绍当Python对象是整型的时候,结构体只需要存储一个值就可以了,如果Python对象是列表的时候,结构体除了存储列表的元素,还需要存储列表的元素个数。

接下来看Python中不同的数据类型对应的结构体分别会封装什么变量。
在这里插入图片描述

1.2 引用计数器

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

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

在C源码中两个关键的结构体PyObject和PyVarObject,PyObject结构体中存放的是公共的数据类型,PyVarObject是基于多个元素组成的对象保存的方法。因为Python3中int型数据的存放类似于字符串,所以也是用PyVarObject结构体进行存储。

每个对象创建一个结构体的时候,都会在结构体中存在引用计数变量,创建结构体的时候默认引用计数的值为1;当有其它变量引用这个对象的时候,引用计数器就会发生变化。

举个例子,下面的Python的代码:

a = 999
b = a

程序运行的时候,a会创建一个结构体然后放进双向链表中,此时引用计数为1,当程序运行b=a时,a创建的结构体的引用计数会变为2。

可以使引用计数加1当然也可以让引用计数减1,例如下面的Python代码:

a = 999
b = a
del b

del b代码的意思是删除变量b,同时b指向的结构体的引用计数器的值减1。如果这时候继续写Python代码del a,则a对应的结构体的引用计数器的值继续减一,这时候引用计数器的值变为0.

在Python中,有一个简单的逻辑,当结构体的引用计数器为零时,意味着没有人使用这个结构体变量,意味着这个对象是垃圾了,这时候就可以对这个对象执行垃圾回收了。

回收是做两件事,一是将对象从双向链表中删除,第二个是将这个对象进行销毁,销毁意味着将内存归还给系统。

以上就是在Python内部引用计数器的大体机制,因为Python中还有缓存机制,缓存机制中并不是当引用计数为零就销毁,而是会做成一个缓冲,下面会进行介绍。

2、标记清除

上面介绍的引用计数器有一个bug,存在循环引用问题。举个例子,看一个Python代码:
在这里插入图片描述
上图代码中,创建了一个v1列表对象,创建了一个v2列表对象,这样会对应在双向链表中创建两个结构体,对应的引用计数都为1。当使用v1.append(v2)之后,这样v1的第三个索引指向列表[44,55,66],同时v2也指向[44,55,66],这样列表[44,55,66]对应的引用计数器就应该变为2;同理,v2.append(v1)也会使得[11,22,33]对应的引用计数变为2;

当执行del v1,会使得v1对应的引用计数器减一,同理,del v2会使得v2对应的引用计数器减一。这时候两个结构体的引用计数都为1,因为不为零,所以这两个结构体都不会被垃圾回收。

但是这样代码就会存在一个问题,代码中已经把v1和v2删除了,这时候已经没有任何变量指向[11,22,33]和[44,55,66],那这两个链表在以后的代码中也无法被使用,代码中无法被使用,这两个结构体应该是垃圾,但是这两个结构体的引用计数器还是1,Python的垃圾回收机制又不会将其作为垃圾进行回收。由于存在这个矛盾,因此这两个链表结构体会一直存在在双向链表中,永远不会被回收。当这种代码越来越多时,会导致程序一直在运行,但是内存一直在被消耗,一直到最后内存爆栈,最后只能重启电脑。

所以单单使用引用计数来进行垃圾回收也不是完美的,会出现循环引用导致内存泄漏。所以在Python的底层不会单单引用计数器,为了解决循环引用问题,Python内部又引入了另外一个机制叫标记清除

标记清除引入的目的是为了解决引用计数器循环引用的不足,标记清除是怎么实现的呢?标记清除会在Python底层再去维护一个链表,链表中专门放那些可能存在循环引用的对象。

那什么对象可能会产生循环引用呢?我们可以知道,只有那些元素中可以存放其它元素的对象才可能出现循环引用问题,例如list的append()。所以列表、字典、元组、集合等都可能存在循环引用问题,针对这种类型的对象会再维护一个链表来进行存储。

在这里插入图片描述

我们知道,内存中本来就有一个双向链表,当我们使用标记清除维护一个链表时,内存中就有会两个链表,一个链表是refchain,其功能和单单使用引用计数是一样的,不做改变。而标记清除中维护的链表只存储可能会出现循环引用的对象。

比如现在创建一个val = 19对象,因为这个对象不存在循环引用问题,因此只放在refchain中;如果维护一个列表对象,因为列表可能存在循环引用问题,因此列表对象不仅会存放在refchain中,还会存放在标记清除的链表中。

在Python中,在某种情况下,会扫描可能存在循环引用的链表的每个元素,检查是否存在循环引用,如果存在循环引用,则让各自的引用计数器值减一,当减一之后引用计数值为0,则这个对象为垃圾,需要进行垃圾回收。

3、分代回收

上面说了引用技术和标记清除,标记清除会在某种情况下触发,检查是否存在循环引用。那么在什么情况下会使用标记清除扫描对应的链表呢?还有一个问题是检查是否存在循环引用时耗时可能比较大。这是标记清除的两个问题,那我们怎么解决这两个问题呢?

在Python的垃圾回收机制和内存管理中,又引入了另外的一个话题,分代回收
在这里插入图片描述

在分代回收中规定了一项新的技术,将可能会存在循环应用的对象维护为三个链表,分别叫0代,1代和·2代,每一个都是一个双向链表;另外还规定了多久对这些链表扫描一次,当0代中的对象达到700个就扫描一次;当0代扫描10次,则1代扫描一次;当1代扫描10次时,2代扫描一次。

当我们创建一个列表对象的时候,对象会存储在refchain中,也会存放在可能存在循环应用的链表中的0代,然后再创建一个列表对象的时候,也会继续存放在0代中,直到0代到达700个之后。当0代的对象达到700个之后会对0代里面的元素做一次扫描,如果存在循环引用,则对应的引用计数减一,如果减一变为0则进行垃圾回收。如果不是垃圾就将这个数据从0代升级到1代,这样第一遍扫描完0代之后,如果元素是垃圾就进行回收了,如果不是垃圾则存储到1代中,这样0代中就没有数据了,这时候1代会进行标记,标记0代已经扫描了一次。当Python再创建对象的时候,会继往0代中存放元素,0代达到700之后再对0代进行垃圾回收,以此类推,当0代循环十次之后,就对对1代执行扫描。

分代回收解决了标记清除的两个问题,一个是什么时候扫描的问题,二是解决了扫描数据量大的问题,将其分成三代,以此提升扫描效率。

4、小结

在Python中维护了一个refchain双向环状链表,这个链表存储程序创建的所有对象,每种类型的对象中都有一个ob_refcnt值,也就是引用计数器,这个值维护着引用的个数,当有多个值进行引用时,引用计数器的个数会进行加一操作或者建减一操作。最后当引用计数器变为零时会进行垃圾回收。

但是在Python中对于可以由多个元素组成的对象可能存在循环引用的问题,为了解决这个问题,Python引入了标记清除和分代回收,在其内部维护四个链表,分别是refchain,还有分代0代,1代和2代。在源码的内部,当分代各链表的元素达到各自的阈值时则触发扫描链表进行标记清除,有循环则各自减一。

但是,Python源码内部在上述流程中提出了优化机制,这个机制就是缓存机制。

5、缓存机制

5.1 池

为了避免重复创建和销毁一些常见对象,Python会维护一个池,维护一个池意思是在内部创建好对象,以后使用对象的时候不用重复创建。

举个例子,写个Python代码:

v1=7
v2=9
v3=9

按理说,运行上面代码,Python会分别创建三个对象,然后加到refchain链表中。但是因为7和9是经常用到的常量,所以在Python内部启动解析器的时候,Python内部会帮我们创建一些值,默认创建从-5到256的值,Python会认为这些值是我们常用的值,所以Python会在内存中帮我们把这些值进行创建。

这时候当执行v1=7,在Python内部就不会新创建一个对象放到refchain中,而是直接去池中获取对象,然后把这个对象放到refchain中。同理,v2=9也是一样的,也不会重新开辟,而是直接从池里面拿已经创建好的对象,这就是Python的缓存机制。由于这个缓存机制,所以在执行v3=9,在Python内部不会重新开辟内存,v2和v3都指向9,v2和v3都会去池里拿9这个对象放进refchain中,因此v2和v3指向的内存地址是一样的。

但是池只创建了一个范围内的对象,当创建的对象大于257时,Python就会重新开辟内存,比如v4=666,v5=666都会重新开辟内存,而且v4和v5的内存地址是不一样的,这和上面的v2和v3内存地址一样是不同的。

因此创建池能够提升Python的效率,这就是池的缓冲机制。

5.2 free_list

当一个对象引用计数器为零时,按理应该进行垃圾回收,但是在Python内部为了进行优化,并不会对其进行回收,而是将对象添加到一个free_list的列表中,当成缓存。当以后创建对象时,不再重新开辟内存,而是直接使用free_list。

举个例子,输入以下Python代码:

v1 = 3.14
del v1

当运行这段代码会创建一个float对象,并将其加到refchain中。接下来执行删除操作,这时候v1的引用计数器变为零,这时候理应对v1执行垃圾回收,将其从refchain中进行摘除,并从内存中进行消除。但是实际上,Python会将这个对象存放到free_list中。当以后创建一个变量v9=999.99,这时候Python就不会重新开辟内存,而是直接从free_list获取这个对象,获取到对象之后对对象中的数据进行初始化,再放到refchain中,这也是一种优化机制。

free_list不会把每一个对象都放进来,free_list假设最多能放80个,当del了80个,这些对象都会存放在free_list中,当del第81个对象时,因为free_list已经满了,因此不会缓存到free_list中,这个时候才会对对象进行销毁。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值