Python内存管理&垃圾回收机制

发布时间 2023-10-01 22:02:37作者: 七落安歌

Python内存管理&垃圾回收机制

引用计数器为主,标记清除和分代回收为辅 (循环垃圾回收器) + 缓存机制

一、引用计数器

1、环状双向链表 refchain

image-20231001191547777

  • 在python程序创建的任何对象都会放在rechain双向链表中。
name = '七落'
age = 18
hobby = ['篮球', '美女']
# 内部会创建一些数据【上一个对象、下一个对象、类型、引用个数】
name = '七落'

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


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

2、类型封装结构体

data = 3.14

# 内部会创建:
	_ob_next = refchain中下一个对象 
    _ob_prev = refchain中上一个对象
    ob_refcnt = 1 # 引用个数
    ob_type = float
    ob_fval = 3.14
  • 前四个都是对象中共有的值:PyObject结构体(四个值)

3、引用计算器

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

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

在C源码中有两个关键结构体:PyObject(通用)、PyVarObject。

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

  • 引用:
a = 9999 # 创建对象时初始化引用计数器为1
b = a # 9999的引用计数器 + 1
  • 删除引用
a = 9999
b = a
del b # b变量删除,b对应对象(9999)引用计数器 -1
del a # a变量删除,a对应对象(9999)引用计数器 -a

# 垃圾回收:当一个对象的引用计数器为0的时候,意味着没有人使用这个对象了,这个对象就是一个垃圾,垃圾回收
# 回收:1.对象从refchain双向链表中移除 2.将对象销毁,内存归还

增加引用计数器:

  • 对象被创建时

  • 另外的别名被创建

  • 被作为参数传递给函数(新的本地引用)

  • 成为容器对象的一个元素

减少引用计数器:a = 10

  • 对象的引用被销毁时 (del a)
  • 当变量被赋值给另外的对象 (a = 10,b = a,a = 20)

二、标记清除

1、循环引用问题

循环引用&交叉感染

v1 = [1,2,3] # 在refchain创建列表对象v1,初始化引用计数器为1
v2 = [4,5,6] # 在refchain创建列表对象v2,初始化引用计数器为1

v1.append(v2) # 把v2追加到v1中,则v2对应的[4,5,6]对象的引用计数器+1,最终为2
v2.append(v1) # 把v1追加到v2中,则v1对应的[1,2,3]对象的引用计数器+1,最终为2

del v1 # 引用计数器为-1 
del v2 # 引用计数器为-1 
  • 引用计数器并不能为0,如果只是通过引用计数器的话并不能销毁对象,归还内存

2、标记清除

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

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

image-20231001200956183

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

问题:

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

三、分代回收

image-20231001202021973

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

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

比如:当0代中的对象个数达到700时候扫描一次,将存在的循环引用对象的引用计数器-1,若是引用计数器为0则垃圾回收,否则将对象存放到1代中管理。当0代扫描10次后,1代扫描一次,同样存放到2代中管理。

四、小结

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

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

  • refchain
  • 0代:700个对象
  • 1代:0代扫描10次
  • 2代:1代扫描10次

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

五、Python缓存

1、池(int)

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

# 在启动解释器的时候,python内部帮我们创建:-5、-4...257
v1 = 10 # 内存不会开辟内存,直接去池里面取
v2 = 16 # 内存不会开辟内存,直接去池里面取,引用计数器的本来已经被初始化为1了

2、free_list(float/list/tuple/dict)

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

v1 = 3.14 # 开辟内存,内部存储结构定义值,并存储到rechain结构中

del v1 # 从refchain中移除,把对象添加到 free_list中,free_list满了则销毁

v2 = 9.99 # 不再重新开辟空间,去free_list中获取对象,对象内部数据初始化,再放到rechain中

# 那么v1 与 v2 可能有相同的id内存地址

参考文档:

https://www.cnblogs.com/wupeiqi/articles/11507404.html

http://www.wklken.me/posts/2015/09/29/python-source-gc.html