Python内存管理和垃圾回收机制

零 概述

首先说明一下,源码是基于CPython

Python垃圾回收机制是以引用计数为主,标记清除和分代回收为辅,再加缓存机制,以提升Python性能。

一 引用计数

1 环形双向链表 refchain

在 Python 中创建的任何对象,都会加入到一个双向循环链表 refchain 中。对象节点的大致结构如下:

下一个对象
上一个对象
引用计数
对象类型

CPython源码:

// cpython/Include/object.h

// 上一个对象 和 上一个对象
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

// 对象类型
typedef struct _typeobject PyTypeObject;

// 对象节点
typedef struct _object {
    _PyObject_HEAD_EXTRA // 上一个对象 和 上一个对象
    Py_ssize_t ob_refcnt; // 引用计数
    PyTypeObject *ob_type; // 对象类型
} PyObject;

// 集合对象类型
typedef struct {
    PyObject ob_base; // 对象类型
    Py_ssize_t ob_size; // 元素个数
} PyVarObject;

2 对象类型结构体

对于不同的数据类型,python封装了不同的结构体,CPython源码:

// 对象节点 cpython/Include/object.h
#define PyObject_HEAD          PyObject ob_base;
#define PyObject_VAR_HEAD      PyVarObject ob_base;

// 浮点数对象 cpython/Include/floatobject.h
typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

// 整数 cpython/Include/longintrepr.h
struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

// 整数对象 cpython/Include/longobject.h
typedef struct _longobject PyLongObject;

// 元组对象 cpython/Include/cpython/tupleobject.h
typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;

// 列表对象 cpython/Include/cpython/listobject.h
typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

// 字典对象 cpython/Include/cpython/dictobject.h
typedef struct {
    PyPyObject_HEAD
    Py_ssize_t ma_used;
    uint64_t ma_version_tag;
    PyDictKeysObject *ma_keys;
    PyObject **ma_values;
} PyDictObject;

// 集合对象 cpython/Include/setobject.h
typedef struct {
    PyObject_HEAD
    Py_ssize_t fill;
    Py_ssize_t used;
    Py_ssize_t mask;
    setentry *table;
    Py_hash_t hash;
    Py_ssize_t finger;
    setentry smalltable[PySet_MINSIZE];
    PyObject *weakreflist;
} PySetObject;

每种类型都含有相同的部分:PyPyObject,集合类型 PyVarObject 多一个 ob_size,其他的数据每种类型有各自的定义。

当在python中定义一个对象时,Python解释器会在内存中创建一个结构体。例如:

pi=3.14

python 解释器中会创建一个结构体:

struct PyFloatObject floatObject = {
    ._ob_next = null, // 指向环形链表的下一个节点
    ._ob_prev = null, // 指向环形链表的前一个节点
    .ob_refcnt = 1, // 引用计数
    .ob_type = float, // 数据类型
    .ob_fval = 3.14, // 记录值
};

3 引用计数

Python 程序运行时,根据不同的数据类型 创建不同的结构体,然后加入到双向循环链表中。每中类型的结构体含相同部分 PyObject,其中 ob_refcnt 表示引用计数,默认为1,当其他变量引用对象时,引用计数会发生变化。当计数器为0时,被系统回收——从双向链表中移除,然后释放结构体。

  • 增加引用
a = 1 // 默认引用计数为1
b = a // 引用计数加1,变为2
  • 删除引用
a = 1 // 默认引用计数为1
b = a // 引用计数加1,变为2
del b // 引用计数减1,变为1
del a // 引用计数减1,变为0,被系统回收

二 标记清除

看下面这个例子:

l1 = [11, 12, 13] // l1默认引用计数为1
l2 = [21, 22, 23] // l1默认引用计数为1
l1.append(l2) // l2引用计数为2
l2.append(l1) // l1引用计数为2
del l1 // l1引用计数为1
del l2 // l2引用计数为1

当删除变量l1和l2时引用计数没有变为0,但这两个变量不再被使用,变量没有被回收,就会出现内存泄漏。因此当出现循环引用时,单纯使用引用计数,会出现内存泄漏,为了解决这一问题,Python中引入了标记清除。

维护另一个链表,当创建集合对象时(元组,列表,字典,集合),同时加入到此链表中。Python解释器会不定时的扫描这个链表,检测是否存在循环引用,存在则将双方的引用计数减1。

三 分代回收

标记清除有两个问题:

  • 何时扫描
  • 扫描的性能

分代回收回收就是为了解决这个问题。

将可能存在循环引用的集合对象的链表分为3个:

  • 0代:对象达到700时扫描一次
  • 1代:0代扫描10次,1代扫描1次
  • 2代:1代扫描10,2代扫描1次

那么标记清楚的过程就变成了:

  1. 当创建集合对象时,加入双向循环量表的同时,也加入到0代中;
  2. 当0代链表中对象数量达到700时,扫描一次0代,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收,不为0则将对象从0代链表移入1代链表;
  3. 当0代链表扫描10次时,扫描1代链表,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收,不为0则将对象从1代链表移入2代链表;
  4. 当1代链表扫描10次时,扫描2代链表,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收。

四 缓存

1 内存池(int)

为了避免重复创建和消耗一些常见对象,维护了一个常见对象的对象池。

# 在启动解释器时,会创建 -5 到 256 的整数

# 不会开辟内存,直接从内存池中取
v1 = 1
v2 = 1
# 打印出来的值是相同的
print(id(v1), id(v2))

# 超过 256 的数值,会重新开辟内存
v3 = 257
v4 = 257
# 打印出来的值是不相同的
print(id(v3), id(v4))

2 free list(float/char/list/dict/tuple)

当引用计数为0时,对象会被回收,但对于常用变量,引用计数为0时不销毁,而是将其加入到 free list 中;当再创建对象时,不开辟内存空间,而是直接使用 free list 中已有的对象。

pi = 3.14 # 引用计数为1
print(id(pi))
del pi # 引用计数为0,只从refchain中移除,加入到 free list 中,不释放内存
new_pi = 3.14 # 从 free list 取出元素,不重新分配内存
print(id(new_pi)) # 打印出来的值跟 pi 一样

free list 的长度限制:

  • float: 100
  • char: 所有ascii
  • list:80
  • dict:80
  • tuple:20,[[空元组], [长度为1元组], …, [长度为19元组]],每个元素元组最长为2000

3 字符串驻留机制(str)

针对只含有字母、数字、下划线的字符串,如果内存中已存在则不会重新创建,而是使用原来的地址里

str1 = "abc"
str2 = "abc"
print(id(str1),id(str2)) # 打印出来的值相同

五 小结

在 Python 解释器中维护了一个双向循环链表,存储程序中创建的所有对象,每种类型的对象都有一个引用计数,当增加引用时,引用计数加1,删除时引用计数减1,当引用计数为0后,则回收(双向循环量表中移除,并消耗对象);

当集合对象出现循环引用时,引用计数会出错,因此 Python 中引入了标记清除,扫描集合对象是否有现循环引用,有则将双方的引用计数减1。

为了解决标记清除中扫面的时机和性能问题,加入了分代回收机制,维护3个三代集合对象链表,当达到条件才,才扫描对应代的链表。

为了避免重复的开辟和释放内存,python 中引入了缓存机制。


参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值