浅谈python垃圾回收机制

浅谈python垃圾回收机制

一句话概括:引用计数为主,标记清除和分代回收为辅,另外还有缓存机制;

基于C语言源码学习python(3.8.2)的垃圾回收机制;

  • 引用计数器
  • 标记清除
  • 分代回收
  • 缓存机制

在学习python垃圾回收机制的C语言源码之前,需要知道一种数据结构,环状双向链表(refchain)

环状双向链表

实现一个环形的双向链表,链表的每个节点都保存三个信息,当前节点的值value,前一个节点的指针prev,后一个节点的指针next。因为是环形的,所以最后一个节点的next指向第一个节点,而第一个节点的prev指向最后一个节点

如果只存在一个节点,那么这个节点的prev和next都会指向这个节点本身。

在python中创建的任何对象都会被放在这个环状链表中。不同数据类型放到这个链表中的时候里面的内容会有所不同。

name = "张三"
age = 22
info = ["北京市", "13812345678", "程序员"]
new_info = info   # 指向同一个对象,该对象引用次数+1

在双向链表中,保存的信息包括:上一个对象的指针, 下一个对象的指针, 数据类型, 引用次数;不同数据类型,存储的信息不完全相同; 比如,数值类型的对象,还可能将数据的值存储在环状链表中,如果是列表/元组,还需要存储其各项元素以及长度;

对应的C语言源码:

#define PyObject_HEAD		PyObject ob_base;
#define PyObject_VAR_HEAD		PyVarObject ob_base;

// 宏定义,包含 上一个、下一个,用于构造双向链表。
#define _PyObject_HEAD_EXTRA		\
	struct _object *_ob_next;		\
	struct _object *_ob_prev;

typedef struct _object {	// 这个结构体相当于存了4个属性
	_PyObject_HEAD_EXTRA	// 用于构造双向链表,这里相当于上面定义的两行6和7
    Py_ssize_t ob_refcnt;	// 引用计数器
    struct _typeobject *ob_type;	// 数据类型
} PyObject;

typedef struct {
	PyObject ob_base;	// PyObject对象(继承上一个结构体,包含上一个结构体的4个属性)
    Py_ssize_t ob_size;	// Number of items in variable part,即 元素个数
} PyVarObject;

也就是说,在C语言源码中,每个对象都有的属性,包括:上一个、下一个、引用个数、数据类型,保存在PyObject这个结构体中;可以简单理解为这个是基类,继承这个基类之后,就会拥有这些属性。

类似于列表、元组等由多个元素组成的数据类型,在PyObject这个基类拥有的4个属性的基础上,再加上ob_size(元素个数)这个属性,通过“继承”的方式来实现(这里说的“继承”并非真正意义上的继承,举个直白点的例子,一堆易拉罐,可以用胶带将每3个缠一起,也可以将缠好的3个作为整体,再往这3个外面粘贴另一个)。

python中常见的数据类型C语言源码:

// 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


// 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;

举个例子:现在在python里面创建一个浮点类型数值,如:a = 1.2,就会使用float类型对应的结构体,内部创建的属性有:

_ob_next	指向refchain中的下一个对象
_ob_prev	指向refchain中的上一个对象
ob_refcnt = 1
ob_type = float
ob_fval = 1.2
引用计数器

当python程序运行时,会根据数据类型的不同,找到对应的C语言源码中的结构体,再根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双向链表中;

在C语言源码中,有两个关键的结构体,PyObjectPyVarObject,创建一个Python的对象时,如a = 3,引用计数器ob_refcnt默认值为1,当有其他变量引用该对象的时候,比如b = a,引用计数器ob_refcnt就会增加1,此时refcnt=12;如果删除了变量的引用,或者说断开引用,比如使用del b,这个时候引用计数器refcnt就会减少1。

a = 3  # refcnt = 1
b = a  # refcnt = 2
del b  # refcnt = 1
del a  # refcnt = 0

当一个对象的引用计数器为0时,就表示没有变量要引用这个对象,也就是说这个对象就是没用的对象,可以视为垃圾,就要进行垃圾回收,也就是说要将创建对象时申请的内存归还给操作系统(这里暂时先不说缓存机制);

引用计数器存在的问题

循环引用

比如下面的代码:

li_1 = [1, 2, 3]	# refchain中创建一个列表对象,引用计数器refcnt=1
li_2 = [4, 5, 6]	# refchain中创建一个列表对象,引用计数器refcnt=1

li_1.append(li_2)	# li_1中引用了[4, 5, 6], 于是引用计数器+1,此时refcnt=2
li_2.append(li_1)	# li_2中引用了[1, 2, 3], 于是引用计数器+1,此时refcnt=2

del li_1	# 删除1个,refcnt-1 = 1
del li_2	# 删除1个,refcnt-1 = 1

# 此时虽然删除了两个对象,但是由于相互引用,导致无法释放内存空间
标记清除

目的:为了解决python引用计数器循环引用导致内存泄露的缺陷;

实现:在python的底层,再多维护一个链表,链表中存放可能会被循环引用的对象,如list类型、dict类型等;

​ 在特定情况下,扫描可能存在循环引用对象的链表中的每个元素,如果发现循环引用,会让双方的引用计数器都减去1,如果减完之后引用计数器为0,则进行回收;

存在的问题:
  • 特定情况是什么情况?
  • 扫描可能花费大量时间
分代回收

目的:弥补引用计数器的缺陷;

实现:将可能存在循环引用的对象维护成3个环状双向链表;分别为0代、1代、2代;

机制:

  • 0代:0代中对象个数达到700个时,触发扫描操作;
  • 1代:0代扫描达到10次,触发1代扫描操作(不要理解为7000个对象了);
  • 2代:1代扫描达到10次,触发2代扫描操作(不要理解为70000个对象了);

在创建对象时,同时将对象添加到refchain和0代中;在0代中的对象达到700个时,触发扫描,此时对0代中的对象进行扫描,如果发现循环引用,将其refchain中的引用计数器减去1,如果减完之后引用计数器为0,则将其回收,再将剩余没有被回收的对象转移到1代中去;

小结

在python底层中维护了一个refchain的环状双向链表这个链表中存储了程序创建的所有对象,而且保存了每个对象的引用计数器ob_refcnt,当对象被引用时,引用计数器+1,断开引用时,引用计数器-1,当引用计数器变为0时,对象所占用的内存空间会被释放,refchain中也会移除该对象。

但是对于python中的list、tuple、dict等,可能会存在循环引用的情况,又引入了标记清除和分代回收机制;

也就是说在python底层,维护了至少4个链表:

  • refchain
  • 2代
  • 1代
  • 0代

python源码内部上述流程中提出了优化机制:缓存;

内存池

为了避免重复创建和销毁一些常见的对象,python底层还维护了一个内存池,用于对小块内存的申请与释放的管理,如果创建多个小内存对象,频繁调用python的内存分配器(PyMalloc)来分配内存,会大大降低程序性能,所以python底层实现了内存池机制;

内存池机制就是预先在内存中申请大小相等的内存块用于备用,在申请内存时,如果申请的内存没有超过256kb,就会优先从内存池中分配内存,这样做会减少内存碎片,提升内存利用率;当所需内存大于256kb的时候,才会执行malloc来重新分配内存。

另外,在释放内存的时候也是一样,当变量引用数量为0时,将从内存池分配的空间归还,而不是直接释放,这样做也可以减少频繁释放内存导致的性能下降。


以上为参考其他人的观点与个人的见解,如有不当之处,欢迎批评指正,共同进步!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值