引用计数器
首先我们大概回忆一下C语言中的环状双向链表,如图,在双向链表中对于一个结点来说会有前驱和后继:
C语言中基本的定义方式如下:
typedef struct {ElemType data; // 数据域Lnode* prior; // 前驱指针域Lnode* next; // 后继指针域
}Lnode;
现在我们把这段基础代码拿到python中来详细看看会被怎么样拓展编写:
typedef struct _object {struct _object* ob_next; // 下一个元素struct _object* ob_prev; // 上一个元素Py_ssize ob_refcnt; // 引用计数器struct _typeobject* ob_type; // 数据类型 _typeobjec会根据类型替换
} PyObject;typedef struct {PyObject ob_base; // PyObject对象Py_ssize_t ob_size; // 元素个数
} PyVarObject;
在python中,这个环状链表C源码的表示如上,可以发现在它分为PyObject和PyVarObject两个结构体,在python底层C源码中每个类型都有其对应的结构体
PyObject是结点的固定变量(指向上一个的指针、指向下一个的指针、引用计数器、数据类型)构成的结构体
PyVarObject是有多个元素组成的对象(例如一个列表L=['a','b','c'])构成的结构体
我们把目光看到引用计数器,首先我们要知道,上面的这样一个环形双向链表在python中称为refchain双线链表,当python程序执行的时候,会根据不同的类型找到对应的类型,再根据结构体中的字段来创建相关的数据,然后将对象添加到refchain双线链表中
那我们来具体看看是怎么做的,例如在python中,你输入一行代码:a = 1.25,python会帮你创建出这些内容:
输入:a = 1.25创建:
_ob_prev = refchain的上一个对象
_ob_next = refchain的下一个对象
ob_refcnt = 1
ob_type = int
ob_fval = 1.25
- _ob_prev 用于保存上一个对象
- _ob_next 用于保存下一个对象
- ob_refcnt = 1 引用计数器
- ob_type = int 数据类型
- ob_fval = 1.25 数据的值
可以看到由于定义了a=1.25,所以计数器记录了一次,当有其他值引用对象时候,计数器就会发生变化
a = 1.25 # ob_refcnt=1
b = a # ob_refcnt=2del b # ob_refcnt=1
del a # ob_refcnt=0
当我将计数器的值减成0的时候,程序中已经没有人再需要这个对象了,所以,这个对象也就变成了垃圾,那就要进行垃圾回收,程序就会将该对象完全从refchain中销毁,内存也会释放出来
现在你应该开始了解垃圾回收了吧,我们来回忆一下引用计数器和垃圾回收:
因为程序会将我们创建的对象放到refchain环形双向链表中,而该链表的每个对象的结构中都有一个引用计数器ob_refcnt,当你创建的时候默认值就是1了,如果这个对象已经被你删除了,那就没有用即成为垃圾,程序就会进行垃圾回收,将该对象直接从链表中删除,此时该对象被销毁,他占有的内存空间也就被释放了
但是这样的引用计数器并不是万能的,他会面临着循环引用和交叉感染的风险,下面我们看这段代码:
v1 = [1, 2, 3] # v1的ob_refcnt = 1
v2 = [4, 5, 6] # v2的ob_refcnt = 1v1.append(v2) # v2的ob_refcnt = 2
v2.append(v1) # v1的ob_refcnt = 2del v1 # v1的ob_refcnt = 1
del v2 # v2的ob_refcnt = 1
虽然我们已经删除v1,v2,已经不需要使用他们,但是这两个变量始终无法彻底从refchain链表中完全销毁,就会占用多余空间,浪费资源
所以为了解决这个问题,python又引入了标记清除这个方法
标记清除
目的:解决引用计数器的循环引用的不足
实现:在python的底层又去维护了一个新链表,该链表里面专门存储这些有可能存在循环引用的对象
哪些对象可能存在循环引用呢?
如果只是单纯的引用赋值等,基本不会产生循环引用问题,你可以删除该但是当该对象是被另一个对象嵌套使用的时候,你是无法删除这个嵌套引用的操作的,此时就有可能产生循环引用问题,而能包含其他对象的有以下几种:列表、元组、集合、字典,所以,我们可以将这些怀疑对象放入新链表
python会定期去扫描这个新链表,就可以检查当中的元素是否有循环引用,如果有计数器-1后值=0,那就是垃圾,直接将其从refchain链表删除
但是这也有一个新问题:
- 什么时候扫描合适呢?如果一直扫描程序性能会受影响
- 其次是扫描列表这些,每一个子元素都要扫描,耗时就会比较久
为了解决这些问题,又有了分代回收
分代回收
为了解决标记清除的两个问题,分代回收方法将存储可能存在循环引用问题的对象分为三个链表,分别是0、1、2代链表
- 0代:所有可能对象都先存储在0代链表中,当0代链表打到700个数据的时候,程序对该链表做一次扫描,将垃圾对象在refchain中销毁,剩余非垃圾对象全部移到1代链表中,此时1代链表会记录下0代链表扫描了1次,以此类推
- 1代:当0代链表扫描10次后,1代链表扫描1次
- 2代:当1代链表扫描10此后,2代链表扫描1次
这样扫描次数不会过多,同样的数据也无需重复扫描检查过多次
缓存机制
基于以上几步,python对其又做出了优化机制,即为python缓存,缓存基本分为两类,池和free_list
池(int...)
先说第一种缓存机制:池
v1 = 9 # 创建一个对象,变量名为v1,值为9
v2 = 8 # 创建一个对象,变量名为v2,值为8
v3 = 9 # 创建一个对象,变量名为v3,值为9
按理来说,三个变量应该是不同的地址,我们打印出来看看:
print(id(v1), id(v2), id(v3))
# 140737462441000 140737462440968 140737462441000
可以发现v1和v3的值相同于是两者的地址也相同
之所以这样是因为当我们创建一些简单变量的时候,为了避免反复创建销毁,python提前为我们准备好了一些数据存储起来,构成了一个数据池,当我们创建的对象值在这个范围内,程序会去池子里面找到该值,直接拿来用
例如int类型的值有-5~257,9早就已经被创建好放到池子里,v1和v3只是找了同一个地址上的值拿来用,所以两个地址也相同
str类型也会预存ascli字符
str1 = 'A'
str2 = 'A'print(id(str1),id(str2))
# 140737462484992 140737462484992
除此之外,python还给字符串设置了驻留机制,python中有一段常见代码:
str3 = 'python'
str4 = 'python'
print(id(str3) == id(str4)) # True
此时‘python’可不是ascil字符表的内容了,之所以两者地址相同正因为这个驻留机制,使得对于这些只有下划线、数字、字母的简单字符串如果在内存中已经存在,那么再次创建就可以直接用原来地址,而不需要在free_list中存留
free_list(turple/list/set/dict)
l = [1, 2]
l1 = [1, 2]
l2 = [3, 4]print(id(l)) # 4336
print(id(l1)) # 9264
print(id(l2)) # 8752
当我们创建了三个列表,列表之间并不会因为数据相同就有相同地址,说明python并没有像池一样给我们缓存一些列表数据
del l1
del l2l3 = [5, 6]
print(id(l3)) # 8752l4 = [7, 8, 9]
print(id(l4)) # 9264
接着我们删除了l1和l2,添加了新列表l3和l4,可以看到3和2的地址相同,4和1的地址相同,说明3用的是2的空间,4用的是1的空间,也就说明1和2虽然被删除了,但是还没有完全销毁,他们的位置被保存下来了
这个保存他们位置的地方就是free_list,像列表、元组、字典、集合这些数据类型,被删除的时候并不会直接被完全销毁,而是将其保存在free_list中,等到有相同类型被创建的时候,就会占用他们的位置,将内部的值和名字都改为新的
注意:元组在free_list内元组会以不同个数分别保存起来,当释放对应个数的元组时,会在free_list中找相应个数元组的地址
在free_list中:T=([1个元素的元组],[2个元素的元组],[3个元素的元组]....)t1 = (1, 2)
t2 = (3, 4, 5)del t1 # 将t1存放到有2个元素的元组中
del t2 # 将t2存放到有3个元素的元组中t3 = (1) # 去只有1个元素的元组中找位置
t4 = (8, 9) # 去只有2个元素的元组中找位置
此外,该free_list并不是无限的,当它满了一定数量,就不会再缓存了,而是照常直接销毁