python内存管理与垃圾回收机制
Python解释器由c语言开发完成,py中所有的操作最终都由底层的c语言来实现并完成,所以想要了解底层内存管理需要结合python源码来进行解释。
源码中两个重要的结构体
1 |
|
以上源码是Python内存管理中的基石,其中包含了:
- 2个结构体
- PyObject,此结构体中包含3个元素。
- _PyObject_HEAD_EXTRA,用于构造双向链表。
- ob_refcnt,引用计数器。
- *ob_type,数据类型。
- PyVarObject,次结构体中包含4个元素(ob_base中包含3个元素)
- ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
- ob_size,内部元素个数。
- PyObject,此结构体中包含3个元素。
- 3个宏定义
当按照上述方式创建一个Float类型对象时,源码内部会先后执行如下代码。
1 | /* Special free list |
第一步:根据float类型所需的内存大小,为其开辟内存。
1 |
|
第二步:对新开辟的内存中进行类型和引用的初始化
1 | /* Macros trading binary compatibility for speed. See also pymem.h. |
1 | /* Head of circular doubly-linked list of all objects. These are linked |
所以,float类型每次创建对象时都会把对象放到 refchain 的双向链表中。
情景二:float对象引用时
val = 7.8
data = val
这个过程比较简单,在给对象创建新引用时,会对其引用计数器+1的动作。
情景三:销毁float对象时
当进行销毁对象动作时,先后会执行如下代码:
1 | void |
用float类型的tp_dealloc进行内存的销毁。
按理此过程说应该直接将对象内存销毁,但float内部有缓存机制,所以他的执行流程是这样的:
float内部缓存的内存个数已经大于等于100,那么在执行del val
的语句时,内存中就会直接删除此对象。
未达到100时,那么执行 del val
语句,不会真的在内存中销毁对象,而是将对象放到一个free_list的单链表中,以便以后的对象使用。
综上所述,float对象在创建对象时会把为其开辟内存并初始化引用计数器为1,然后将其加入到名为 refchain 的双向链表中;float对象在增加引用时,会执行 Py_INCREF在内部会让引用计数器+1;最后执行销毁float对象时,会先判断float内部free_list中缓存的个数,如果已达到300个,则直接在内存中销毁,否则不会真正销毁而是加入free_list单链表中,以后后续对象使用,销毁动作的最后再在refchain中移除即可。
垃圾回收机制
Python的垃圾回收机制是以:引用计数器为主,标记清除和分代回收为辅。
1. 引用计数器
每个对象内部都维护了一个值,该值记录这此对象被引用的次数,如果次数为0,则Python垃圾回收机制会自动清除此对象。下图是Python源码中引用计数器存储的代码。
2. 循环引用
循环引用的问题会引发内存中的对象一直无法释放,从而内存逐渐增大,最终导致内存泄露。
为了解决循环引用的问题,Python又在引用计数器的基础上引入了标记清除和分代回收的机制。
3. 标记清除&分代回收
Python为了解决循环引用,针对 lists, tuples, instances, classes, dictionaries, and functions 类型,每创建一个对象都会将对象放到一个双向链表中,每个对象中都有 _ob_next 和 _ob_prev 指针,用于挂靠到链表中。
1 | /* Nothing is actually declared to be a PyObject, but every pointer to |
随着对象的创建,该双向链表上的对象会越来越多。
- 当对象个数超过 700个 时,Python解释器就会进行垃圾回收。
- 当代码中主动执行 gc.collect() 命令时,Python解释器就会进行垃圾回收。
个人总结
python是基于c语言开发的,python每创建一个对象,就会在c的结构体中维护几个值,在c源码中,有两个结构体pyobject和pyvarobject,pyobject中有四个参数,双向链表,引用计数器,数据类型,pyvarobject中引用了pyobject结构体和元素个数,比如当我们生成一个int类型的对象i=1,则c会生成五个对象,他的类型为int,引用计数器+1,val=1,然后把它放入双向链表refchain中,c中的malloc函数来开辟内存空间,如果在python中让p=val,那么p就指向了val所在的地址,引用计数器就会加一,在函数调用他的时候,在函数执行的时候引用计数器+1,执行完后引用计数器-1,del p则引用计数器-1,当引用计数器为0时,按理说应该进行垃圾回收,但是在源码中有一个缓存列表,int类型会把-4-256放在缓存列表中,如果下次创建的话会先在缓存列表中寻找,没有就开辟新内存,对于str的话如果长度是0或者1默认都贮存,如果长度大于1并且只含有数字大小写字母下划线的时候会贮存,如果是用乘法得到的字符串,乘数为1时,仅含有大小写字母数字下划线默认贮存,含有其他字符长度大于一或者小于一会默认贮存,乘数大于二的时候仅含有数字字母下划线长度<=20到时候会默认贮存。list是100。
垃圾回收机制是引用计数器为主,标记清除和分带回收为辅
引用计数器就是上面说的,多一个指向他的引用计数器就+1,删除就-1,但是在容器操作的时候会出现循环嵌套的情况,比如lista.append(b),list(b).append(a),这样ab的引用计数器都会1,但是删除的时候引用计数器的值=1但是已经没有参数指向他了这样就会造成垃圾不会被回收,解决方法就是在源码中只要是容器类型的就放在一个列表中定期扫描,如果是垃圾就回收,但是这样会造成每次扫描处理数据量过大,为了优化就用了三个列表,如果多次扫描不是垃圾就会放入下级列表,然后上级列表扫描10次下级列表扫描一次,多次后还不是就会放入下级列表,一共三级,这就是分带回收。