首先要记住一句话:
“Python采用的是引用计数机制为主,标记清除和分代收集两种机制为辅的策略。”
引用计数器
首先来了解一下引用计数器,因为是以引用计数器为主的机制。
1.环状双向链表
在Python中,有一个环状的双向链表叫refchain,任何对象都会被放在这个环状双向链表中。
name = "cat"
age = 10
hobby = ["eat fish", "sleep"]
上面的代码一执行,会创建三个对象,一个字符串对象,一个整型对象,一个列表对象,这三个对象都会被放到这个链表中。
当python代码遇到name = "cat"
,内部会创建一些数据(C语言源码是创建了一个结构体):上一个对象,下一个对象,类型,引用的个数等等,当前对象的类型是字符串,引用的个数是1,因为name这个变量名引用了当前这个对象,如果new = name
,那么这个引用计数会加一。
如果这个对象是由多个元素组成的,还会有一个值记录它的元素个数。
C的源码:
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
// 宏定义,包含 上一个、下一个,用于构造双向链表用。(放到refchain链表中时,要用到)
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA // 用于构造双向链表
Py_ssize_t ob_refcnt; // 引用计数器
struct _typeobject *ob_type; // 数据类型
} PyObject;
typedef struct {
PyObject ob_base; // PyObject对象
Py_ssize_t ob_size; /* Number of items in variable part,即:元素个数 */
} PyVarObject;
如果是不同类型的数据,内部会创建以下的内容:
- float类型
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
- int类型
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
/* Long (arbitrary precision) integer object interface */
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
- str类型
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* Number of code points in the string */
Py_hash_t hash; /* Hash value; -1 if not set */
struct {
unsigned int interned:2;
/* Character size:
- PyUnicode_WCHAR_KIND (0):
* character type = wchar_t (16 or 32 bits, depending on the
platform)
- PyUnicode_1BYTE_KIND (1):
* character type = Py_UCS1 (8 bits, unsigned)
* all characters are in the range U+0000-U+00FF (latin1)
* if ascii is set, all characters are in the range U+0000-U+007F
(ASCII), otherwise at least one character is in the range
U+0080-U+00FF
- PyUnicode_2BYTE_KIND (2):
* character type = Py_UCS2 (16 bits, unsigned)
* all characters are in the range U+0000-U+FFFF (BMP)
* at least one character is in the range U+0100-U+FFFF
- PyUnicode_4BYTE_KIND (4):
* character type = Py_UCS4 (32 bits, unsigned)
* all characters are in the range U+0000-U+10FFFF
* at least one character is in the range U+10000-U+10FFFF
*/
unsigned int kind:3;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
unsigned int :24;
} state;
wchar_t *wstr; /* wchar_t representation (null-terminated) */
} PyASCIIObject;
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
* terminating \0. */
char *utf8; /* UTF-8 representation (null-terminated) */
Py_ssize_t wstr_length; /* Number of code points in wstr, possible
* surrogates count as two code points. */
} PyCompactUnicodeObject;
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;
- list类型
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
- tuple类型
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
- dict类型
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
其中:
- _ob_next : refchian中的上一个对象
- _ob_prev:refchain中的下一个对象
- ob_refnt:引用计数器
- ob_type:是当前对象的类型
- ob_fval:是这个对象的值
当python运行程序时,会根据数据类型的不同找到它对应的结构体,根据结构体中的字段来进行创建相关数据,然后将这个对象添加到refchain中。
大体机制:
每个对象中有ob_refcnt就是引用计数器,默认值为1,当有其他变量引用对象时,引用计数器的值会增加,如果引用这个对象的变量被删除或者引用别的对象了,那么这个引用计数器的值会减小,当引用计数器的值变为0时,意味着没有变量在使用这个对象了,那么这个对象就变成了需要被删除的垃圾,系统就会将这个对象从refchian里面移除,将对象销毁,把这块内存还给系统。
2.单纯使用引用计数器进行垃圾回收的问题:循环引用
如果是这样的一个代码:
# refchian中创建一个列表对象,因为v1=对象,所以对象的引用计数器为1
v1 = [11,22,33]
# 同理为1
v2 = [44,55,66]
# 把v2追加到v1中,那么[11,22,33]的引用计数器就会加一,变为2
v1.append(v2)
# 同理为2
v2.append(v1)
这时候,如果删除了变量v1和变量v2:
# [11,22,33] 的引用计数器减一,变为1
del v1
# [44,55,66]同理为1
del v2
这时候就会有一个问题,就是已经没有变量引用对象[11,22,33]和[44,55,66]了,但是因为他们的引用计数器不为0,这两个对象的内存空间没有被回收,所以他们会永远在内存中,而又永远不会被使用到。
所以为了解决这个问题,就有了标记清除。
标记清除
标记清除就是为了解决引用计数器循环引用的问题。
实习方法就是在Python的底层再去维护一个环形双向链表,这个链表用来存放可能存在循环引用问题的对象。(只有对象可以再放其他元素的对象才会出现循环引用问题,列表,字典,元组和集合)
在Python内部,在某种情况下,回去扫描这个存放可能存在循环引用问题的对象的链表,如果发现有循环引用,就把双方的引用计数器都减一,如果引用计数器减为0,回收内存。
但是标记清除也存在问题:
- 什么时候会扫描一次
- 扫描一次存在耗时久的问题
所以又引入了分代回收。
分代回收
实现方式就是把标记清除的那个链表分成了三个链表,这三个链表分别是:0代,1代,2代。
- 当0代中的对象个数超过700个,扫描一次0代。
- 0代如果扫描10次,则1代扫描一次。
- 1代扫描10次,2代扫描一次。
三个链表的阈值是不同的,0代是对象个数,1代和2代都是前一代的扫描次数。
总结
在Python中维护了一个叫refchain的双向环状链表,这个链表用来存储程序创建的所有对象,每种类型的对象都有一个ob_refcnt引用计数器的值,当有变量引用对象时,引用计数器的值就会加一,当引用对象的变量减少了的时候,引用计数器的值就会减一,当引用计数器变为0时,就会进行垃圾回收。
但是在Python中,对于有多个元素组成的对象,可能还会有循环引用的问题,为了解决这个问题,Python又引用了标记清除和分代回收,所以在Python内部实际上要维护四个双向环状链表,分别是:
- refchian
- 0代 700个
- 1代 0代扫描10次
- 2代 1代扫描10次
在源码内部,当达到各自的阈值时,就会扫描链表,发现循环引用就会想相关对象的引用计数器减一,如果引用计数器的值被减为0,那么这个对象就会被销毁,这个对象占用的内存空间就会被回收。
优化:缓存
在Python内部,源码对上述过程进行了优化,这个优化就是缓存。
1.内存池
为了避免重复创建和销毁一些常见对象,就会维护一个内存池。
在启动Python解释器时,内部会自动为我们创建-5~256这些数字放到内存池中,如果有变量需要指向这些值,内存不会开辟新的内存,直接从内存池中取。
所以如果:
v1 = 8
v2 = 8
打印一下v1和v2的id,会发现他们的id是相同的。
而且内存池中的对象,他们的引用计数器不会变为0,因为初始化的时候,他们的引用计数器就为1,这时候没有变量引用他们,所以当引用他们的变量引用后又不再引用他们的时候,他们的引用计数器也不会变为0。
2.free_list
当一个对象的引用计数器变为0时,先不回收,而是把这个对象放到free_list中,当作缓存,这样再创建对象时,就不开辟新的内存,直接使用free_list中的对象的内存,把这块内存初始化,再把这个对象放到refchain中。
当然这个缓冲池也是有大小限制的,如果一个对象的引用计数器变为0,而此时缓冲池也已经满了,那么这个对象还是会被直接销毁的。
float,list,tuple,dict采用这种机制。
- float类型,维护的free_list链表最多可缓存100个float对象。
- list类型,维护的free_list链表最多可缓存80个list对象。
- dict类型,维护的free_list链表最多可缓存80个dict对象。
tuple类型的比较特殊,可以理解为如果是tuple类型的,free_list的容量为20,这时候的free_list有20个index,index从0到19,每个索引位置都存放了一个链表,index为0的位置,存放的是空元组,index为1的位置存放的是元组长度为1的元组,这样以此类推。每一个链表都可以存放2000个元组。
字符串类型的有两种优化方式:
1.字符池
2.字符串驻留机制
- 字符池就是和int的内存池类似,python内部会将ASCII的所有字符存在一个叫
unicode_latin[256]
的链表中。 - 字符串驻留机制:python会将只有字母、数字、下划线并且长度不大于20的字符串进行驻留,放到内存,如果下次再创建一个一模一样的值,就不再开辟新的内存空间。
网友评论