Python内存管理:垃圾回收

纵然是瞬间 提交于 2020-03-12 17:06:59
前言:
平时没接触过Python,但是关于垃圾回收面试时候被问到了,就学习一下为什么Python用的是引用计数法。

Python GC主要使用引用计数来跟踪和回收垃圾。在引用计数的基础上,通过“标记-清除”解决容器对象可能产生的循环引用问题,通过“分代回收”以空间换时间的方法提高垃圾回收效率。

引用计数

引用计数法在对象内部维护了一个被其他对象引用数的引用计数值,当这个引用计数值为0时,说明这个对象不再被其他对象引用,就可以被回收了。

所有Python对象的头部包含了这样一个结构PyObject(相当于继承自PyObject)

struct _object {
    Py_ssize_t ob_refcnt;
    struct PyTypeObject *ob_type;
} PyObject;

ob_refcnt就是引用计数值

可能会出现两个对象循环引用,导致垃圾回收器都不会回收它们,所以就会导致内存泄露。

优点: 高效;运行期没有停顿;对象有确定的生命周期;易于实现
缺点:
    维护引用计数的次数和引用次数成正比,而不像标记-清除那样与回收的内存数量有关。
    无法解决循环引用的问题。

标记-清除

“标记-清除”法是为了解决循环引用问题。在申请内存时,所有容器对象的头部又加上了PyGC_Head来实现“标记-清除”机制。

typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;

垃圾标记时,先将集合中对象的引用计数复制一份副本(以免在操作过程中破坏真实的引用计数值)。
操作这个副本,遍历对象集合,将被引用对象的引用计数副本值减1。(减1后如果副本值=0说明只被外部引用了一次)
根据引用计数副本值是否为0将集合内的对象分成两类,可达和不可达,其中不可达是可以被回收的对象。在处理了弱引用和一些其他细节之后,就可以回收不可达中的对象了。

分代回收

将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。

用来表示“代”的结构体是gc_generation, 包括了当前代链表表头、对象数量上限、当前对象数量

struct gc_generation {
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
              generations */
};

Python默认定义了三代对象集合,索引数越大,对象存活时间越长。新生成的对象会被加入第0代,前面_PyObject_GC_Malloc中省略的部分就是Python GC触发的时机。每新生成一个对象都会检查第0代有没有满,如果满了就开始进行垃圾回收。

gc模块

gc模块提供一个接口给开发者设置垃圾回收的选项。它的一个主要功能就是解决循环引用的问题。

  • 项目中避免循环引用
  • 引入gc模块,启动gc模块的自动清理循环引用的对象机制
  • 由于分代收集,所以把需要长期使用的变量集中管理,并尽快移到二代以后,减少GC检查时的消耗
  • gc模块唯一处理不了的是循环引用的类都有__del__方法,所以项目中要避免定义__del__方法,如果一定要使用该方法,同时导致了循环引用,需要在代码中显式调用gc.garbage里面的对象的__del__来打破僵局
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!