python(垃圾回收机制)

参考link1
参考Link2
参考link3源码解析
参考link3 bilibili
python默认采用的垃圾收集机制是『引用计数法 Reference Counting』。Python中实现对象的基石PyObject,有两个属性,一个是该对象的类型,还有一个就是引用计数(ob_refcnt)。
在这里插入图片描述
环状双向链表refchain
在这里插入图片描述
在python程序中创建的任何对象都会放在refchain链表中。

name="撒"
内部会创建一些数据,【上一个对象,下一个对象,类型,引用个数】
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
//宏定义,包含上一个,下一个,用于构造双向链表用,(放到refchain链表中要用)
#define _PyObject_HEAD_EXTRA
	struct _object *_ob_next;
	struct _object *_ob_prev;
	
typedef struct _object
{
	_PyObject_HEAD_EXTRA//用于构造双向链表
	Py_ssize_t ob_refcnt;//引用计数器
	struct _typeobject *ob_type;//数据类型
}PyObject;

typedef struct
{
	PyObject ob_base;//PyObject对象
	Py_ssize_t ob_size;//元素个数
}PyVarObject;

类型封装结构体

在C源码中每个对象有相同的值:PyObject结构体(4个值)
有多个元素组成的对象(list):PyObject结构体(4个值)+ob_size

float类型
typedef struct
{
	PyObject_HEAD
	double ob_fval;
}PyFloatObject;

引用计数

原理:每个对象维护一个ob_ref字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_ref加1,每当该对象的引用失效时计数ob_ref减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。

缺点:
1、需要额外的空间维护引用计数
2、不能解决对象的 “循环引用”。

typedef struc_object
{
	int ob_refcnt;
	struct_typeobject *ob_type;
}PyObject;

引用计数加1

1、对象被创建 :a=14
2、对象被引用:b=a
3、对象被作为参数,传到函数中 : func(a)
4、对象作为一个元素,存储在容器中 : List={a,”a”,”b”,2}

引用计数减1

1、当该对象的别名被显式销毁时:del a
2、当该对象的引别名被赋予新的对象:a=26
3、一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
4、将该元素从容器中删除时,或者容器被销毁时

我们还可以通过sys包中的getrefcount()来获取一个名称所引用的对象当前的引用计数(注意,这里getrefcount()本身会使得引用计数加一)

sys.getrefcount(a)

在这里插入图片描述
在内部,创建一个对象时,Python总是在对象的C结构体里保存一个整数,称为引用数。期初,Python将这个值设置为1:
在这里插入图片描述
值为1说明有一个指针指向或是引用这三个对象。假如我们现在创建一个新的Node实例,JKL:
在这里插入图片描述
与之前一样,Python设置JKL的引用数为1。然而,请注意由于我们改变了n1指向了JKL,不再指向ABC,Python就把ABC的引用数置为0了。

此刻,Python垃圾回收器立刻挺身而出!每当对象的引用数减为0,Python立即将其释放,把内存还给操作系统:
在这里插入图片描述
现在来看第二例子。加入我们让n2引用n1:
在这里插入图片描述
上图中左边的DEF的引用数已经被Python减少了,垃圾回收器会立即回收DEF实例。同时JKL的引用数已经变为了2 ,因为n1和n2都指向它。

循环引用

lst1 = []
lst2 = []
lst1.append(lst2)
lst2.append(lst1)
del lst1, lst2

初始的时候,lst1和lst2指向的内存的引用计数都为1;

但是lst1.append(lst2),那么lst2指向内存的引用计数变成了2,同理lst2.append(lst1)导致lst1指向内存的引用计数也变成了2;

因此当我们del lst1, lst2的时候,引用计数会从2变成1,因此lst1和lst2都不会被回收,但我们是希望回收lst1和lst2的。

因此此时我们就说lst1和lst2指向的对象之间发生了循环引用,所以如果只是引用计数的话,那么显然这两者是回收不了的。

标记-清除

目的:为了解决对象的循环引用问题
实现:在python的底层再维护一个链表,链表中专门放可能存在循环引用的对象(list/tuple/dict/set)

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

问题:
1、什么时候扫描?
2、可能存在循环引用的链表扫描代价大,每次扫描耗时久。

『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。
在这里插入图片描述

原理:
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

我们把小黑圈视为全局变量,也就是把它作为 root object,从小黑圈出发,对象 1 可直达,那么它将被标记,对象 2、3 可间接到达也会被标记,而 4 和 5 不可达,那么 1、2、3 就是活动对象,4 和 5 是非活动对象会被 GC 回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。

分代回收

Python将可能存在循环引用的对象维护成3个链表:
分别为年轻代(第0代):0代中对象个数达到700个扫描一次。
中年代(第1代):0代扫描10次,1代扫描一次
老年代(第2代):1代扫描10次,2代扫描一次

import gc
gc.get_threshold
#输出(700,10,10)

gc.get_count() 
//获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。

小结

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

但是,在python中对于那些可以有多个元素组成的对象可能会存在循环引用问题,为了解决这个问题,python又引入了标记-清除和分代回收,在其内部维护4个链表:
refchain
2代,10次
1代,10次
0代,对象700
在源码内部当达到各自的阈值时,就会触发扫描链表进行标记清除的动作(又循环则各自-1)

python缓存

池(int)

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

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

print(id(v2),id(v3))
//id(v2)=id(v3)

free_list(float/list/tuple/dict)

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

v1=3.14
#开辟内存,内部存储结构体中定义那几个值,并存到refchain中
del v1
//refchain中移除,将对象添加到free_list中。
//free_list满了则销毁


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

针对不同对象的缓存机制见视频
link 26:06
link

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值