概览:
主要通过 引用计数来进行垃圾收集, 就是说,当一个对象没有被其他对象引用的时候,会释放掉内存。
但是会有一些循环引用的对象,通过上面的方法,是没有办法清除掉的。所以,python还有另外的一个机制来解决这个问题,那就是标记-清除。
标记-清除:
主要过程为, 扫描所有容器对象(不会扫描int, string,这些简单对象,因为他们不能包含其他对象的引用,不会造成循环引用),通过一种方法将这些对象分为两部分,一部分表示可以被删除,一部分表示不可被删除,然后将可以被删除的对象回收掉。
那么首先一个问题,如何组织这些container对象呢?python采用了双向链表来跟踪这些对象,所有的container对象在创建之后,都会被插入到链表里。每个container对象里面都会有一个 PyGC_HEAD结构。
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
double dummy; /* force worst-case alignment */
} PyGC_Head;
这个结构中,很明显,gc_next 和 gc_prev 是实现双向链表的两个指针, gc_refs在后面解决双向引用的时候会用到。
通过上面的方法,每当有container对象被创建,就会将他加入到内部维护的可收集对象链表里。这样每次执行垃圾收集的时候,就可以遍历这些链表,进行标记清除啦。但是这样又存在一个问题,在执行垃圾收集的时候,程序是被暂停执行的,垃圾收集结束之后才会继续执行。 每次执行垃圾收集,都需要遍历所有的container对象,如果当前进程中对象比较多的话,会影响程序的执行效率。 一个优化的方法就是:分代收集
分代收集
分代收集基于这样一个统计事实: 程序执行期间,有一部分内存块会在很短的时间分配,然后又被释放。而那些存活时间越久的内存块就越不容易被释放,甚至可能在程序执行期间一直存活,事实上这部分所占的比例还不小。所以,python将系统中存活的内存块根据其存活时间将其分为三个不同的代(年轻代, 青年代, 老年代)。每一个代对应上面描述的一个双向链表。新创建的container对象会被加入到年轻代中,如果他经历了几次回收之后依然存活,那么就把他放入青年代。依次类推。这样可以降低老年代和青年代的扫描频率。因为越老的内存块,扫描之后可以被释放的几率越小, 所以优先扫描年轻的代。
每一个代(双向链表)所容纳的对象个数是有限的,当超过了这个限制,就会触发一次 标记-清除 的过程。目前的版本(python3.5.2, 年轻代是700, 青年代和老年代是10)
那么,如何进行标记-清除的过程呢。比如我们要对青年代进行一次垃圾收集。我们需要从这个链表中的对象中 找到那些被可收集链表以外的对象引用的对象放入 root object集合。然后从这些root object开始遍历可收集链表,从中找出需要删除的对象(也就是存在循环引用的对象)放入unreachable集合里面。然后对unreachable集合中的对象执行回收。 如何找到哪些对象是循环引用的呢?假设对象A,和 B 的引用计数都为1,但是A引用了B,B同时引用了A,所以A和B是可以被回收的。我们需要识别出这种对象,把他放入unreachable集合里。所以我们首先需要解除摘除循环引用,我们首先遍历可收集链表,将其中每个对象的引用计数都减1,这样A和B的引用计数都变为了0。这样,剩下的对象中,如果他的引用计数仍然大于0,说明这个对象不可被删除(因为不止有一个对象在引用他),我们把它放入root object集合里。(ps:这里有个问题,假入对对象C的引用计数减1,这时候对象C的引用计数为0,但是其实C是有其他对象在引用的,那岂不是出问题了,这里就用到了上面PyGC_Head 里面的gc_refs, 也就是并不是直接操作对象C的引用计数,而是拷贝了一个副本,这个变量就是用来干这个的)。
现在我们有了一个root object集合,这个集合里的对象是不能被删除的。所以,这些对象引用的对象也是不可被删除的。现在只需要遍历可收集链表,就可以找到unreacheble对象。完成标记之后,执行回收。
注意:
当python中自己实现了 __del__() 方法时,对于这样的对象,跟据 Python 的定义,在释放该对象占用的资源前需要调用该函数。由于 Python 的垃圾回收机制不能保证垃圾回收的顺序,可能在删除 b 之后,在 a.__del__() 中仍然会调用 b 对象,这样将会造成异常。
为此,Python 采取了比较保守的策略,也就是说对于当自定的类,如果存在 __del__() 时,不会对该对象进行垃圾回收。这样的对象,Python 会直接放到一个 garbage 列表中,这个列表在运行期间不会释放,对于 Python 来说不会有内存泄露,但是对于该程序来说,实际上已经发生了内存泄露