本篇介绍Python内存管理以及垃圾回收机制
文章脉络:
- 底层C的实现 ——> python的内存管理机制 ——> 垃圾回收机制
底层
python是由C开发的,在python中我们常常提到的一句话是“一切皆对象”,那么这个对象在C中是怎样体现的呢?接下来我们慢慢道来
三个重要的结构体
打开python源文件目录,在include这个目录中,
Object.h
中放着具体实现。在include目录下 object.h 找到 PyObject 的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/* Define pointers to support a doubly-linked list of all live heap objects. */
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA /* 用于构造双向链表 */
Py_ssize_t ob_refcnt; /* 引用计数器,Py_ssize_t是int、long或long long的别名 */
struct _typeobject *ob_type; /* 数据类型,它指向的是一个PyTypeObject,PyTypeObject是用来指定一个对象类型的类型对象。在对象建立之前,对象元信息必须存在。 */
} PyObject;
typedef struct {
PyObject ob_base; /* PyObject对象 */
Py_ssize_t ob_size; /* Number of items in variable part 指定容器中包含的元素数量 */
} PyVarObject;
/* ps:
PyObject ob_base; 这句也相当于宏定义中的 PyObject_HEAD
#define PyObject_HEAD PyObject ob_base;
*/
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" 类型的名称 */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation 创建该类型对象时分配的内存大小信息 */
......
/* Method suites for standard classes */
// 标准类方法集
PyNumberMethods *tp_as_number; // 数值对象操作
PySequenceMethods *tp_as_sequence; // 序列对象操作
PyMappingMethods *tp_as_mapping; // 字典对象操作
......
} PyTypeObject;
对于
PyObject
,在Python内部,每一个对象都拥有相同的对象头部,也就使得对对象的引用变得非常统一,我们只需要用一个PyObject*
指针就可以引用任意一个对象,而不论该对象实际上是一个什么对象。对于
PyVarObject
,它是PyObject
的扩展,对于任何一个PyVarObject
所占用的内存,开始部分的字节的意义和PyObject
是一样的对于
PyTypeObject
,它决定对象的类型,这也是python是动态语言的原因,不需要声明,但我的内部已经携带了类型的标识
获取这3个成员的值
1 |
- 通过宏来获取属性,使用 PyObject 来强制转换 ob 后拿到它的引用 ob_refcnt 、 ob_type 、ob_size
关于引用计数
1 |
|
宏 Py_INCREF(op) 和Py_DECREF(op) 用于增加或减少引用计数,当refcount降为0时,py_decf 调用对象的 deallocator 函数;
宏 _Py_NewReference(op) 将引用计数器初始化为1
ps:上面使用宏实现了编译时的多态
基础数据类型的分类
- 在python中所有东西创建对象的时候,内部都会存储一个数据。
- 如果由多个元素组成,内部会在多维护一个 ob_size
1 | PyObject: float |
内存管理机制
1 | 在创建对象时,如: |
- 在创建对象时,每个对象至少内部有4个值:双向链表/ob_refcnt/ob_type,之后会对内存中的数据进行初始化,初始化本质:引用计数器=1,赋值。然后将结构体添加到双向链表中refchain。
- 以后再有其它变量指向这个内存,则让引用计数器 + 1,如果销毁某个变量,则找到它指向的内存,将其引用计数器 - 1.
- 引用计数器如果为0,则进行垃圾回收。
- 在内部可能存在缓存机制,例如:float /list/int(小数据池?-257),录入float最开始不会真正销毁,而放在free_list的链表中,以后再创建同类型的数据时,会先去链表中取出结构体,然后再对结构体进行初始化(100/80)
垃圾回收机制
引用计数为主,标记清除和分代回收为辅。
引用计数
上面我们提到了引用计数,但是对于容器类型(列表、字典、集合、对象等)会出现循环引用问题
1
2
3
4
5
6
7a = [1,2]
b = [5,6]
a.append(b) # b 的计数器为2
# b 发生变化了(多了一个指向) (a指向的内存不变)
b.append(a)
del a
del b
标记清除
- 针对容器类的对象,再python中会将它们单独放到一个(总共3个 0 1 2 共3代)双向链表中,做定期的扫描,检查是否有循环引用,如果有,引用计数各自-1,如果-1之后等于0,则直接回收。
- 扫描10次后,如果没有出现的话,将放到下一个列表中
分代回收
- 解决扫描的时候,少扫描结构体,将没有问题的结构体放到上一级链表中,默认下一级扫描10次,上一代才扫描1次,总共有3代。
- 3个链表处理容器,一个链表处理非容器(float)
小结
- “一切皆对象”的本质在于 PyObject(PyVarObject是对PyObject的扩展),只需要一个
PyObject*
指针就可以引用任意一个对象,而不论该对象实际上是一个什么对象。 - 引用计数对于容器类对象存在循环引用问题,通过标记清除来解决,为了提交效率,使用分代回收来降低扫描频率