文章目录
【学习笔记】Python内存管理&垃圾回收
1.引用计数器
1.1 环状双向链表 refchain
在python程序中创建的对象都会放在refchain链表中。
# 内部会创建【refchain中上一个对象、refchain中下一个对象、类型、引用个数】
name = "coco"
# 内部会创建【refchain中上一个对象、refchain中下一个对象、类型、引用个数、val=22】
age = 22
# 内部会创建【refchain中上一个对象、refchain中下一个对象、类型、引用个数、items=元素、元素个数】
hobby = ["篮球","编程"]
C源码体现每个对象的相同部分——PyObject结构体(四个值)。
有多个元素组成的对象:PyObject结构体 + ob_size(元素个数)。这两部分构成PyVarObject
1.2 类型封装结构体
#define PyObject_HEAD PyObject ob_base;
#define PyObject_Var_HEAD PyVarObject ob_base;
// float类型
typedef struct{
PyObject_HEAD
double ob_fval;
}PyFloatObject;
1.3 引用计数器
v1 = 3.14
v2 = 999
v3 = (1,2,3)
当python程序运行时,会根据不同的数据类型找到对应的结构体根据结构体中的字段创建相关数据,而后将对象添加到refchain双向链表中。
每个对象中有ob_refcnt就是引用计数器,值默认是1,当有其他变量引用此对象时,引用计数器发生变化。
-
引用
a = 1 b = a
-
删除引用
del b # b对应对象引用变a量引用计数器-1 del a # a对应对象引用变量引用计数器-1 # 当引用计数器=0时,启动垃圾回收 # 回收:1.对象从refchain链表中移除 2.将对象销毁,内存归还。
1.4 循环引用
v1 = [1,2,3]
v2 = [4,5,6]
v1.append(v2) # v2的引用计数器为2
v2.append(v1) # v1的引用计数器为2
del v1 # v1的引用计数器为1
del v2 # v2的引用计数器为1
# 此时代码不再使用v1和v2但却不启动gc,两个会常驻在内存中
# 为了解决此问题引入标记清除
2.标记清除
目的:为了解决引用计数器的循环引用的问题
实现:将可能存在循环引用的对象存到一个链表中(list/tuple/set/dict)
Python会在某些情况下扫描此链表,检查是否存在循环引用的情况,如果有就让引用双方的引用计数器都-1;如果变成0就出发gc。
问题:
-
什么时候扫描
-
因为链表中每一个元素的子元素都要扫一遍,代价大,耗时久
3 分代回收
将存循环引用的链表维护成3个链表
- 0代:0代中对象如果达到700个扫描0代一次
- 1代:0代扫描10次,1代扫描一次
- 2代:1代扫描10次,2代扫描一次
是垃圾就剔除,不是垃圾就升级往上一代。
4 缓存机制
源码在上述流程中提出了优化机制——缓存机制。
4.1 池
为了避免重复创建销毁一些常见对象,维护一个池。
# 启动解释器时,python内部帮我们创建:-5, -4, ..., 257
v1 = 7 # 内部不会开辟内存,直接去池中获取
v2 = 9 # 内部不会开辟内存,直接去池中获取
# 当再写一个v3 = 9,v3和v2内存地址是一样的
4.2 free_list(float/list/tuple/dict)
当一个引用计数器为0时,按理说应该回收。但python内部不会直接回收,而是将对象添加到free_list链表当中。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list。
v1 = 3.14 # 开辟内存,并加入refchain
del v1 # 从refchain移除,对象添加到free_list中,free_list中的元素有个数限制
v2 = 9 # 不会重新开辟内存,而是去free_list中获取对象,对象内部数据重置,再放到refchain中
4.3驻留机制
Python内部针对字符串做了驻留机制,针对字母数字下划线,且长度不大于20。如果内存中已存在则不会重新创建而是使用原来地址,原来的值。