引用计数器为主
标记清除和分代回收为辅+缓存机制
1. 引用计数器
1.1 环状双向链表refchain
python程序创建的任何对象都会放在rechain链表中
name = "Adsa"
age = 17
hobby = ["ads", "dasads"]
python内部会为这些对象创建一个结构体[上一个对象,下一个对象,类型,引用个数]
比如
age = 17 结构体包括[上一个对象,下一个对象,类型,引用个数, val=17]
hobby = ["ads", "dasads"] 结构体包括[上一个对象,下一个对象,类型,引用个数, items=元素, 个数]
不同类型创建的结构体包含的内容不一样
在C源码中, 每个对象对应结构的都有的值: PyObject结构体[上一个对象,下一个对象,类型,引用个数]
有多个元素组成的对象: PyObject结构+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 = 333
v3 = (1,2,3)
当python程序运行时,会根据数据类型的不同找到其对应的结构体,根据结构体中的字段进行创建相关数据, 然后将对象添加到refchain双向环状链表中.
C源码中有两个关键的结构体:PyObject, PyVarObject.
每个对象对应的结构体中有ob_refcnt就是引用计数器,默认值为1, 当有其它变量引用对象时, 引用计数器会发生变化.
a = 999
b = a
def b # b变量删除, b对应的对象(也是a对应的对象)引用计数器-1
def a # a变量删除, a对应的对象引用计数器-1
# 当引用计数器为0时, 意味着没有人再使用这个对象了, 这个对象就是垃圾, 可进行垃圾回收
# 回收: 1. 对象从refchain链表中移除. 2. 将对象销毁, 内存归还(但是还有缓存机制)
1.4 循环引用问题
当v1和v2删除后,理论上v1和v2对应对象的引用计数器应该为0, 但是实际上的引用计数器为1, 它将永远在内存, 为解决循环引用问题, 引入标记清除.
2. 标记清除
目的:为了解决循环引用问题
实现:在python的底层再维护一个链表, 链表中专门放那些可能存在循环引用的对象(比如:list/tuple/set)
在python的内部, 某种情况下, 会去扫描可能存在循环引用的链表中每个元素, 检查是否出现循环引用, 如果有, 则让双方的引用计数器-1, -1后引用计数为额0, 则垃圾回收.
问题:
- 什么时候扫描?
- 可能存在循环引用的链表扫描的代价大, 每次扫描耗时久.
3. 分代回收
将可能存在循环引用的对象维护成3个链表:
0代: 阈值: 0代中对象个数达到700个扫描一次.
1代: 阈值: 0代扫描10次, 则1代扫描一次.
2代: 阈值: 1代扫描10次, 则2代扫描一次.
扫描过程中, 如果是循环引用, 则让双方的引用计数器-1, 当引用计数器值为0, 则是垃圾, 可回收, 不是垃圾则升级放到下一代中, 当0代扫描10次, 则1代扫描一次, 依次类推.
4. 小结
在python中维护了一个refchain的双向环状链表, 这个链表存储了程序中创建的所有对象, 每种类型的对象都有一个ob_refcnt引用计数器的值, 引用个数+1, -1, 当引用计数器变为0时, 则会进行垃圾回收(对象销毁, 从refchain移除).
但是在python中, 对于那些可以有多个元素的对象可能会存在循环引用的问题, 为了解决这个问题, python又引入了标记清楚和分代回收, 在其内部维护了4链表.
-
refchain --存放所有对象, 而下面的3个链表只存放可能发生循环引用的对象
-
2代 1代扫描10次
-
1代 0代扫描10次
-
0代 700个
在源码的内部, 当达到各自的阈值时, 就会触发扫描链表, 进行标记清除操作(有循环引用, 则各自-1)
但是,源码内部, 在上述的流程中提出了优化机制.—缓存
5. 缓存
5.1 池
为了避免重复创建和销毁常见对象, python维护了一个池.
# 在启动解释器时, python内部帮我们创建:-5, -4, ..., 256
v1 = 7 # 内部不会开辟内存, 直接去池中获取 注意: 此时7对象的引用计数器为2
v2 = 9 # 内部不会开辟内存, 直接去池中获取 注意: 此时9对象的引用计数器为2
v3 = 7 # 内部不会开辟内存, 直接去池中获取 注意: 此时7对象的引用计数器为3
print(id(v1), id(v3)) # 这里的v1和v3的id应该是一致的
v4 = 666 # 内部会开辟内存, 因为池中没有
5.2 free_list
当一个对象的引用计数器为0时, 按理该对象应该回收, 但是实际上, 内部不会直接回收, 而是将对象添加到free_list链表中缓存. 以后再去创建对象**(相似类型)**时, 不再重新开辟内存, 而是直接使用free_list.
v1 = 3.14 # 开辟内存, 内部会创建对应的结构体, 并加入到refchain中.
def v1 # 从refchain中移除, 将对象添加到free_list中, 当缓冲池满了, 则销毁
v2 = 9.99 # 不会再重新开辟内存, 去free_list中获取对象, 进行对象内部初始化, 再放到refchain中
# 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
print(id(v1), id(v2)) # 这里的v1和v2的id应该是一致的
注意, free_list是有个数限制的
str类型,维护unicode_latin1[256]
链表,内部将所有的ascii字符
缓存起来,以后使用时就不再反复创建。
# 相当于有个字符池
v1 = "A"
print( id(v1) ) # 输出:4517720496
del v1
v2 = "A"
print( id(v2) ) # 输出:4517720496
# 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
v1 = "wupeiqi"
v2 = "wupeiqi"
print(id(v1) == id(v2)) # 输出:True
tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。
v1 = (1,2)
print( id(v1) )
del v1 # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
v2 = ("武沛齐","Alex") # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
print( id(v2) )
v1 = (1,2)
print( id(v1) )
del v1 # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
v2 = ("武沛齐","Alex") # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
print( id(v2) )
参考文献: https://pythonav.com/wiki/detail/6/88/