引用计数器为主,标记清除和分代回收为辅
1 引用计数器
在python程序运行时,会根据数据类型的不同找到其对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双像链表中,每个对象中的ob_refcnt就是引用计算器,值默认是为1,当有其他的变量引用对象时,引用计数器就会发生变化。
当一个对象的ob_refcnt为0时,会进行垃圾回收,将对象从refchain链表中移除,将对象销毁,内存回收
2 标记清除
标记清除存在的问题:循环引用
为了解决循环引用的问题,python引入了标记清除的技术:在python底层再维护一个链表,链表中专门存放可能存在循环引用的的对象(list/tuple/dict/set),在python内部的某种情况下
下触发会去扫描标记情况中的每个元素,检查是否有循环引用如果有则让双方的引用计数-1,refcnt为0则垃圾回收。
3 分代回收
标记清除存在两个问题
- 什么时候去扫描标记清除的链表
- 扫描标记清除的链接扫描的代价大,每次扫描耗时比较久
将标记清除的链表分为三代
- 0代:0代中对象个数达到700个扫描一次
- 1代:0代扫描达到10次,则1代扫描一次
- 2代:1代扫描10次,则2代扫描一次
分代清除解决什么时候去扫描和扫描耗时的问题,当0代的对象个数达到700时会扫描0代计数为0的垃圾回收,否则升级为1代,当0代扫描了10次时会扫描1代,计数器为0清除,否则升级为2代
4 python缓存机制
为了优化python对内存的使用底层使用了多种缓存机制
4.1 小整数池
a = 10
b = 10
print(a == b) # True
print(a is b) # True
python 中经常使用的一些数组定位为小整数池,小整数池的范围为**-5~256**,python对这些数值已经提前创建好了内存地址,即使多次重新定义也不会重新开辟新的空间,但小整数池外在重新定义时会再次开辟新的空间,锁小整数池中的内存地址时相同的
4.2 不可变类型缓存
a = 100000000000000000
b = 100000000000000000
print(a == b) # True
print(a is b) # True
a = "aaa"
b = "aaa"
print(a == b) # True
print(a is b) # True
a = 12.2
b = 12.2
print(a == b) # True
print(a is b) # True
a = (1, 2, 3)
b = (1, 2, 3)
print(a == b) # True
print(a is b) # True
pyhon解释器启动时会先从内存中开辟出来一小块内存,用于存储高频使用的数据(不可变类型,数字、字符串、元组),这样可以大大减小使用数据的对象时申请内存和销毁内存的开销。在同一块代码下,不可变类型的对象被多个变量引用,不会重复开辟内存空间,可变类型(字典、列表、集合)被多个变量创建时会重新开辟新的内存地址
而交互模式下,不会使用缓存机制
4.3 字符驻留
#交互模式
>>> s1='hello'
>>> s2='hello'
>>> print(s1 is s2)
True
字符串作为python最为常用的数据类型,python为了提高字符串的使用效率和使用性能,使用了intern(字符串驻留)来提高字符串的效率,即使同样的字符串对象仅仅会保存一份,放在一个和字符串储存池中,是共用的,有新的变量引用同样的字符串时候,不会开辟新的内存空间,而是引用这个共有的字符串。所以在交互模式下字符也是同一个对象
满足字符驻留的条件
- 在交互模式下,只包含字母数字下划线的字符串才会触发 intern 机制
- 在 IDE 环境或者脚本模式下,只要长度不超过20(长度限制),即使使用特殊字符也会触发 intern 机制
- 用的是 python 3.9,发现没有长度限制了,都会触发 intern 机制
4.4 free_list
当一个对象的引用计算器为0时,按道理说应该回收,但是内部不会直接直接将开辟的内存空间直接回收,而是将对象放在free_list链表中当缓存,以后再去创建对象时不再重新开辟内存而是直接使用free_list的对象。(float/list/tuple/dict)
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中,这个时候才会对对象进行销毁。
5 源码分析
5.1 创建float对象
var = 3.14
创建一个float对象的时候
- 首先会去查询free_list中是否为空,为空会malloc一块新的内存空间,否则从free_list中获取一块内存。
- 之后对PyObject初始化如引用计数器初始化为1,添加到refchain链表中
- PyObject对象的ob_fval进行赋值
- 返回PyObject的指针
5.2 引用 float对象
val = 3.14
val1 = val
出现引用的时候,会将原来PyObject中的ob_refcnt+1
5.3 销毁float对象
var = 3.14
del var
销毁对象时,会将引用计数器-1,如果引用计数器为0则执行Dealloc进行垃圾回收,否则啥事情都不做