一句话总结
python中一切皆对象,python使用引用计数标记对象,当某个对象上的引用计数为0的时候,就将该对象回收,为了解决循环引用问题,python辅以可达分析,同时,根据统计学规律,大多数内存中的对象都是短生命周期的、处于新生代,python也使用了分代收集的方法回收内存。
如果不想了解详细的内容的话,那么了解到此就可以了。
引用计数
python中一切皆对象,在python虚拟机的实现看来,一切都是PyObject,其结构体如下
1 | |
其中的ob_refcnt就是这个对象的引用计数,当这个值为0时,即没有任何其他对象引用该对象,那么就代表这个对象可以被回收。
那么很容易就想到了循环引用的问题,当A引用了B,B也引用了,并且A、B都没有被其他对象引用,那么A、B的ob_refcnt都是1,没法被回收,但实际上,这俩个对象,没有被外部所引用,是应该被回收的
可达性分析
为了解决循环引用问题,python引入了可达性分析。
容器对象
想想,如果对象本身只是一个基础对象,比如一个int对象,那么这个对象是不存在对其他对象的引用,像这样的对象就不会有循环引用问题,那么就不用可达性分析,在python中,将存在对其他对象引用的对象,叫做容器对象。
可收集对象链表
python会把这些容器对象,用一个双向链表串起来,其实现如下,这个结构体包括的双向链表的俩头指针
typedef struct {
// Pointer to next object in the list.
// 0 means the object is not tracked
uintptr_t _gc_next;
// Pointer to previous object in the list.
// Lowest two bits are used for flags documented later.
uintptr_t _gc_prev;
} PyGC_Head;
这个PyGC_Head会被虚拟机自动加到容器对象的头前面,即一个容器对象的结构如下
1 | |
可达性分析
可达性分析需要从一个根对象出发,逐步标记可达的对象和不可达的对象,然后回收不可达的对象,那么怎么确定根对象?
读过《Python源码剖析》的话,就知道,早期的PyGC_Head结构体中,就有一个gc_refs的值,这个值实际上被称为有效引用计数,是ob_refcnt在可收集对象链表中对该对象的引用的数,即从A出发,检索A对象包含的引用,发现A对象引用了B,那么就把B的ob_refcnt减去1,存入gc_refs,那么再从B出发,发现B引用了A,那么A的gc_refs就等于A的ob_refcnt减去1,这时,A和B的gc_refs都等于0,实际上摘除了循环引用。那么扫描一边可收集对象链表,找到那些gc_refs不为0的对象,实际上就是根对象。
题外话:《Python源码剖析》这本比较早了,分析的python版本好像是2.4,我在网上听说,作者本来像一直更新的,后来去挽救公司,就没时间出续作,纸质书都不再出版了,只能搜电子版了,但是对于理解python原理还是有很大帮助的,建议结合当前的源码去阅读,实际上我就是这么做的,哈哈哈哈
但是,当前的PyGC_Head没有gc_refs这个值了,那么是更换了找根对象的方法了吗?
实际上并没有,看看_gc_prev的注释Lowest two bits are used for flags documented later.,这个值的后俩位是后面记录的标志。再看看源码
//可达性分析函数
visit_reachable(PyObject *op, PyGC_Head *reachable)
{
...
// 这里还是使用了gc_refs
const Py_ssize_t gc_refs = gc_get_refs(gc);
...
}
gc_get_refs(PyGC_Head *g)
{
//直接从_gc_prev取出后俩位
return (Py_ssize_t)(g->_gc_prev >> _PyGC_PREV_SHIFT);
}
果然,gc_refs从没有消失,只是隐藏在_gc_prev之中而已。
分代收集
python中,默认是分三代,对应的是三个可收集对象链表,存放generations数组中
struct gc_generation {
PyGC_Head head;
int threshold; /* collection threshold */
int count; /* count of allocations or collections of younger
generations */
};
struct gc_generation generations[NUM_GENERATIONS];
python将从generations中,查找count值越界的最老的那一代,然后将着代之前的所有代,和当前代合并,实际就是合并链表,然后在这个合并后的链表上,回收内存
弱引用
如果读过python的源码的话,就会发现很多结构体中,都有存放弱引用对象的链表,比如
1 | |
这里为什么要提弱引用呢,因为弱引用是不会增加ob_refcnt的值的,所以,开发者可以使用弱引用来加快可达性分析。具体的使用,这里就暂且不提了。
唠叨俩句
相比较与java的内存回收,就会发现python的内存回收简单很多,总感觉python就是怎么实现简单怎么来,选择引用计数回收内存也是基于这个理念,但是实际的内存回收效率,就很难说了有多高效。
┑( ̄Д  ̄)┍