Python源码之内存管理机制(二)---垃圾回收机制

本文详细探讨了Python内存管理中的垃圾回收机制,包括引用计数、三色标记机制和垃圾收集过程。引用计数是Python中最基本的内存管理方式,但无法处理循环引用问题。为了解决这个问题,Python引入了三色标记机制和分代回收,通过标记-清除和分代策略有效处理循环引用,确保内存得到及时回收。文章还介绍了Python如何维护对象的双向链表以跟踪垃圾回收,并讨论了分代回收的原理。
摘要由CSDN通过智能技术生成

1、引用计数

  在之前的文章中我们都有看到各种与引用计数相关的东西,Python和Java,c#一样也在语言层面实现了动态的内存管理机制,这一点我们很清楚,这就意味着我们从繁琐的手动内存管理中解放了,将内存的释放和各种管理交给Python自己完成,我们可以看到在前面的内存管理架构中,手动管理内存虽然有很大的灵活性但是非常繁琐。
  在Python中,大多数对象的生命周期都是通过引用计数来完成的,它是一种最简单,而且最容易实现的一种垃圾收集机制。它在对象进行内存的分配,释放的时候都会有管理引用计数的操作,引用计数有一个非常明显的优点,就是实时性很强,对于任何一块内存,一旦没有指向它的引用,它就会立即被回收。同时它也有致命的一个弱点,就是无法处理循环引用的情况。

  • 1.1 循环引用问题

  当一个对象的引用被创建或者被复制的时候,引用计数会加1,同样当一个对象的引用被销毁时,它的引用计数就会减1. 当一个对象的也能用计数减少为0的时候,就意味着,没有人引用这个对象,这个对系那个所占用的内存就需要被释放。我们来考虑一下下面的情况:

l1 = []
l2 = []

l1.append(l2)
l2.append(l1)

del l1, l2

可以看到这两个对象没有被任何外部变量所引用,它们只是相互引用,因此它们的引用计数都不为0,这就意味着即使没有外部变量引用它们,但是由于引用计数不等于0,它们所占用的内存空间就没办法被释放。这是一个严重的问题,这和内存泄露没有什么分别。虽然这个问题可以在语言层面去避免,但是这就要求开发者必须自己精心设计代码结构从而避免这个问题,如此一来那为什么我不去选择一种不存在这个问题的语言呢?因而Python为了解决这个问题,引入了标记–清除和分代回收的技术。

2、三色标记机制

  在垃圾收集的过程中,一般都分为两个过程:其一是垃圾的检测,就是从已经分配的内存中寻找出哪些是可以回收的,哪些是不可以回收的;其二就是垃圾的回收,就是让系统重新掌握被检测出来的可回收内存块。 我们接下来就看看标记–清除是如何工作的。
  标记–清除的过程如下:
1、寻找根对象(root object)的集合,所谓的root object就是一些全局引用和函数栈的引用。这些引用所用的对象是不可被删除的,而这个root object集合也是垃圾检测动作的起点
2、从root object集合出发,沿着root object集合中的每一个引用,如果能到达某个对象A,则称A是可达的(reachable),可达的对象也不可被删除。这个阶段就是垃圾检测阶段
3、当垃圾检测阶段结束后,所有的对象分为了可达的(reachable)和不可达的(unreachable)。而所有可达对象都必须予以保留,而不可达对象所占用的内存将被回收。

  现在我们来看看三色标记模型是怎么样被建立起来的。首先在触发回收动作之前,我们将系统中所有分配的对象以及对象之间的引用组成一个有向图,每个对象为有向图的结点,对象之间的引用为有向图的边。在垃圾回收动作前假设所有的对象都是不可达的,有向图中的所有结点被标记为白色。在某个时刻,垃圾回收开始,这时沿着root object集合中的某个引用链,到达了对象A,表示它可达,我们将它标记为灰色,它表示它所包含的引用还没有被检查。当A所包含的引用被检查了后,A被标记为黑色,表示它所包含的引用已经全部被检查了,这时A所包含的引用的对象被标记为灰色。因此在某个时刻它可能的状态如下图所示:
在这里插入图片描述

3、垃圾收集机制

  我们知道在Python的内存管理中,主要还是靠引用计数,而像标记–清除和分代回收机制只是为了解决循环引用问题而引入的辅助技术。因此在Python的垃圾回收机制中,只有存在循环引用的对象才会被跟踪,对于像整数类型,字符串这样的类型它们内部不存在引用其他对象的情况。那么哪些对象会出现循环引用呢?例如,list,dict,set,instance等容器。因此垃圾回收机制的开销仅仅作用于这些对象中,那么要对他们进行检测收集就必须跟踪这些对象的引用情况,也就是说我们必须将这些对象采用某种集合集中起来,以便后续的检测,Python采用了一个双向链表来组合它们,当这些对象被创建时,就会被插入到链表中。

  • 3.1 对象集合–双向链表

  如果一个可收集的对象要被垃圾收集机制跟踪就必须将它加入到链表中去,因此这个对象内必须要包含某些信息被作用于这个链表。可是我们知道在一个对象中分为PyObject_HEAD头部信息和数据本身,因此这个额外的信息在哪里呢?这个信息就在头部之前被定义为:PyGC_Head

// objimpl.h
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;

// modules/gcmodule.c
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
   
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
   
    return _PyObject_GC_Alloc(0, basicsize);
}

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
   
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);
    if (g == NULL)
        return PyErr_NoMemory();
    g->gc.gc_refs = 0;
    // 设置gc_refs的值为GC_UNTRACKED
    _PyGCHead_SET_REFS(g, GC_UNTRACKED);
    // 将0代链表的数量加1
    generations[0].count++; /* number of allocated GC objects */
    // 触发收集操作
    if (generations[0].count > generations[0].threshold &&
        enabled &&
        generations[0].threshold &&
        !collecting &&
        !PyErr_Occurred()) {
   
        collecting = 1;
        collect_generations();
        collecting = 0;
    }
    op = FROM_GC(g);
    return op;
}

  我们可以看到在创建对象时,在_PyObject_GC_New()函数中调用了_PyObject_GC_Malloc()最后又调用了_PyObject_GC_Alloc()。Python在为可收集的对象申请内存空间时也为Py_GC_HEAD申请了内存,而且在对象本身之前,因此其内存结构就很明显了。
  在垃圾收集机制运行过程中,会根据一个对象的PyGC_Head地址来计算获得PyObject_HEAD地址,同样有时也需要根据PyObject_HEAD地址或计算获得PyGC_Head的地址。

//gcmodule.c
//AS_GC,根据PyObject_HEAD得到PyGC_Head
#define AS_GC(o) ((PyGC_Head *)(o)-1)
//FROM_GC,从PyGC_Head那里得到PyObject_HEAD
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
//objimpl.h
#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)

  从前面关于PyGC_Head的定义中我们知道,有两个指针用于创建可收集对象的链表,但是在上面的代码中我们并没有看到有将这个对象链入到链表中的操作,那么这个操作是在什么时候完成的呢?这个操作是在创建对象的最后一步,从前面我们解析Python内建对象的时候在创建对象的最后一步会有一个函数调用–PyObject_GC_Track(),这个函数中为一个宏,这个宏定义如下,就是用来追踪对象的,也就是将它加入到链表中去,我们来看看原型:

// objimpl.h
#define _PyObject_GC_TRACK(o) do { \
    PyGC_Head *g = _Py_AS_GC(o); \
    if (_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED) \
        Py_FatalError("GC object already tracked"); \
    _PyGCHead_SET_REFS(g, _PyGC_REFS_REACHABLE); \
    g->gc.gc_next = _PyGC_generation0; \
    g->gc.gc_prev = _PyGC_generation0->gc.gc_prev; \
    g->gc.gc_prev->gc.gc_next = g; \
    _PyGC_generation0->gc.gc_prev = g; \
    } while (0);

在将这个对象链入到链表中后,垃圾收集机制就会作用于这个链表,从而创建的对象就在追踪之下了。Python不仅可以将一个刚创建的对象链入到链表中,还可以从链表中摘除,摘除动作就发生在对象被销毁的时候。

  • 3.2 分代回收机制

  在人们对垃圾回收机制的研究过程中发现,不管对于什么语言,不同对象的生命周期会存在不同,有的对象所占的内存块的生命周期很短,而有的内存块的生命周期则很长,甚至可能从程序的开始持续到程序结束。这两者的比例大概在80~90%。试想这样一个问题,之前我们讲了关于标记-清除的机制,现在所有的可收集的对象都在这条双向链表中,每次在进行垃圾回收的时候都要进行垃圾检测的操作将所有的对象都检测一遍,实际上按照前面我们所说的研究结果,有很多对象是比较稳定的,没有必要每次都对它进行检测,因为每次检测带来的额外开销都是不小的,因此我们可以采用一种以空间换取时间的策略来解决这个问题。
  其主要思想是,我们根据对象的存活时间将其分为不同的集合,每一个集合称为一个 “代” ,垃圾收集的频率随着 “代” 的增加而减少,换句话说就是,存活时间越长的对象就越不可能成为垃圾,应该尽量减少收集的频率。而我们通过每个对象经历了几次垃圾收集动作来衡量其存活时间,如果一个对象经历的垃圾收集的动作越多,它的存活时间就越长。在Python中,一个 “代” 就是一个链表,并且Python一共划分了三个代,因此在内部存在三个链表,每一个链表都代表一个代,在前面讲过的可收集对象的链表,为了支持分代的机制,只需要额外一个表头

// gcmodule.c
struct gc_generation {
   
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
                  generations */
};
#define NUM_GENERATIONS 3
#define GEN_HEAD(n) (&generations[n].head)

/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值