Пришло время поговорить о главной функции сборщика мусора в CPython. В предыдущих частях (1, 2, 3) мы говорили о поколениях, инкрементальной и полной сборке мусора — но все они в итоге вызывают главную функцию, которая и реализует основной алгоритм циклического сборщика мусора — находит и разрывает циклы у объектов, которые уже вышли из использования.

В первой статье на тему устройства сборщика мусора я написал, что история началась, когда я попробовал исправить ошибку в CPython. И вот, в четвёртой статье, я наконец‑то добрался до функции, в которой была ошибка.

Если вам интересно, давайте посмотрим, как работает «сердце» сборщика мусора.

Оглавление

  1. Главная функция сборки мусора

  2. Детали реализации

    1. Определение недостижимых объектов

    2. Инициализация временного счётчика ссылок

    3. Подсчёт входящих ссылок

    4. Разделение достижимых и недостижимых объектов

    5. Определение транзитивно достижимых объектов

    6. Почему перемещать недостижимые эффективнее

    7. Дерегистрация кортежей

    8. Объединение с целевым списком

    9. Несобираемые объекты

    10. Обработка слабых ссылок с обратными вызовами

    11. Вызов деструкторов

    12. Обработка воскрешённых объектов

    13. Очистка слабых ссылок

    14. Обработка циклов

  3. Заключение

Главная функция сборки мусора

Все три типа сборки мусора — сборка для молодого поколения, инкрементальная сборка и полная сборка мусора в итоге используют единый механизм, реализованный в gc_collect_region.

Замечание о версии

Главной функцией сборки мусора является gc_collect_region. Такое имя она получила в версии 3.14, в более ранних версиях надо искать gc_collect_main. Но в более ранних версиях gc_collect_main содержит дополнительную функциональность, связанную с, например, с выбором поколения, вызовом колбеков и тому подобное В 3.14 соответствующая функциональность вынесена в _PyGC_Collect.

У этой функции две задачи — найти недостижимые объекты и разорвать циклы, если такие имеются. Среди недостижимых необходимо определить, какие объекты могут быть безопасно освобождены, а какие нет. Первые надо освободить, вторые переместить в мусор. Достижимые же объекты должны быть перемещены в просмотренную часть старого поколения.

В целом всё выглядит очень просто, но тонкости в деталях.

Детали реализации

Как и прежде, я не буду рассматривать отладочные проверки и сбор статистики, а сконцентрируюсь только на функциональной части.

/* This is the main function. Read this to understand how the
 * collection process works. */
static void
gc_collect_region(PyThreadState *tstate,
                  PyGC_Head *from,
                  PyGC_Head *to,
                  struct gc_collection_stats *stats)
{
    PyGC_Head unreachable; /* non-problematic unreachable trash */
    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
    PyGC_Head *gc; /* initialize to prevent a compiler warning */
    GCState *gcstate = &tstate->interp->gc;

    gc_list_init(&unreachable);
    stats->candidates = deduce_unreachable(from, &unreachable);
    untrack_tuples(from);

    /* Move reachable objects to next generation. */
    if (from != to) {
        gc_list_merge(from, to);
    }

    /* All objects in unreachable are trash, but objects reachable from
     * legacy finalizers (e.g. tp_del) can't safely be deleted.
     */
    gc_list_init(&finalizers);
    // NEXT_MASK_UNREACHABLE is cleared here.
    // After move_legacy_finalizers(), unreachable is normal list.
    move_legacy_finalizers(&unreachable, &finalizers);
    /* finalizers contains the unreachable objects with a legacy finalizer;
     * unreachable objects reachable *from* those are also uncollectable,
     * and we move those into the finalizers list too.
     */
    move_legacy_finalizer_reachable(&finalizers);
    /* Print debugging information. */
    if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) {
        for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) {
            debug_cycle("collectable", FROM_GC(gc));
        }
    }

    /* Invoke weakref callbacks as necessary. */
    stats->collected += handle_weakref_callbacks(&unreachable, to);

    /* Call tp_finalize on objects which have one. */
    finalize_garbage(tstate, &unreachable);
    /* Handle any objects that may have resurrected after the call
     * to 'finalize_garbage' and continue the collection with the
     * objects that are still unreachable */
    PyGC_Head final_unreachable;
    gc_list_init(&final_unreachable);
    handle_resurrected_objects(&unreachable, &final_unreachable, to);

    /* Clear weakrefs to objects in the unreachable set.  See the comments
     * above handle_weakref_callbacks() for details.
     */
    clear_weakrefs(&final_unreachable);

    /* Call tp_clear on objects in the final_unreachable set.  This will cause
    * the reference cycles to be broken.  It may also cause some objects
    * in finalizers to be freed.
    */
    stats->collected += gc_list_size(&final_unreachable);
    delete_garbage(tstate, gcstate, &final_unreachable, to);

    /* Collect statistics on uncollectable objects found and print
     * debugging information. */
    Py_ssize_t n = 0;
    for (gc = GC_NEXT(&finalizers); gc != &finalizers; gc = GC_NEXT(gc)) {
        n++;
        if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE)
            debug_cycle("uncollectable", FROM_GC(gc));
    }
    stats->uncollectable = n;
    /* Append instances in the uncollectable set to a Python
     * reachable list of garbage.  The programmer has to deal with
     * this if they insist on creating this type of structure.
     */
    handle_legacy_finalizers(tstate, gcstate, &finalizers, to);
}

На вход функции передаётся исходный список объектов — это может быть полностью молодое поколение, инкремент или объединённый список молодого и старого поколений, и целевой список.

Целевой список, это, как правило, просмотренная часть старого поколения и туда попадут объекты, которые будут признаны достижимыми (это не значит, что они навсегда останутся такими, это значит, что в текущий момент времени их состояние такое).

После выполнения функции исходный список становится условно пустым. Почему условно? Потому что есть разница, в том, как вызывается gc_collect_region для разных типов сборки — подробнее обсудим ниже.

Итак, первое, что происходит в gc_collect_region это определение, какие объекты считаются достижимыми, а какие нет. Алгоритм рассматривает то��ько объекты из исходного списка, никакие транзитивно достижимые объекты не проверяются. Определение достижимых и недостижимых объектов выполняется функцией deduce_unreachable. После окончания её работы из исходного списка в список недостижимых unreachable будут перемещены недостижимые объекты, достижимые останутся в исходном списке.

Определение недостижимых объектов

Основная функция для определения недостижимых объектов — decuce_unreachable. Если убрать все комментарии и отладочные вызовы, то функция выглядит следующим образом:

static inline Py_ssize_t
deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
    Py_ssize_t candidates = update_refs(base);  // gc_prev is used for gc_refs
    subtract_refs(base);

    move_unreachable(base, unreachable);  // gc_prev is pointer again
    return candidates;
}

Инициализация временного счётчика ссылок

Для инициализации временного счётчика ссылок используется функция update_refs.

static Py_ssize_t
update_refs(PyGC_Head *containers)
{
    PyGC_Head *next;
    PyGC_Head *gc = GC_NEXT(containers);
    Py_ssize_t candidates = 0;

    while (gc != containers) {
        next = GC_NEXT(gc);
        PyObject *op = FROM_GC(gc);
        if (_Py_IsImmortal(op)) {
            assert(!_Py_IsStaticImmortal(op));
            _PyObject_GC_UNTRACK(op);
           gc = next;
           continue;
        }
        gc_reset_refs(gc, Py_REFCNT(op));
        _PyObject_ASSERT(op, gc_get_refs(gc) != 0);
        gc = next;
        candidates++;
    }
    return candidates;
}

Эта функция копирует текущее значение счётчика ссылок объекта (Py_REFCNT) во временный счётчик ссылок. Им выступает указатель _gc_prev, копирование осуществляется функцией gc_reset_refs. В первой статье мы разбирали, как устроен заголовок объектов, поддерживающих сборку мусора. Сейчас нам важно помнить, что все объекты связаны в двусвязный список через маркированные указатели _gc_prev и _gc_next. Во время работы сборщика мусора мы можем использовать эти указатели для технических нужд, как в нашем случае.

Помимо этого, при обходе списка исключаются бессмертные (Immortal) объекты. Почему исключение бессмертных объектов происходит в сборщике мусора? Потому что существует эффект Accidental Immortalization, описанный в PEP-638 (или в статье). Суть его в том, что при достижении определённого значения счётчика ссылок объект считается бессмертным. На текущий момент бессмертные объекты являются системными объектами — это глобальные константы, интернированные строки, интернированные числа, статические типы. Такие объекты иммортализируются посредством вызова _Py_SetImmortal, в этой функции объект исключается из сборщика мусора. В случае же Accidental Immortalization этот вызов не происходит, объект считается бессмертным по факту значения счётчика ссылок и продолжает находиться в сборщике мусора. Поэтому и необходима эта проверка при инициализации временного счётчика. Стоит упомянуть, что подобные проверки мы видели при разборе инкрементального сборщика мусора.

Стоит ещё обратить внимание на проверку assert(!_Py_IsStaticImmortal(op));. В чём разница с _Py_IsImmortal(op)? _Py_IsStaticImmortal используется как раз для проверки, что объект не был создан изначально бессмертным, а считается таковым только из‑за значения счётчика ссылок.

Также у всех объектов, которые остались в списке после предыдущей проверки, устанавливается бит PREV_MASK_COLLECTING (это делается в функции gc_reset_refs). Он указывает на то, что объект находится в состоянии сборки мусора. Это позволяет отделить объекты из других поколений, или объектов, которые были добавлены в текущее поколение после начала сборки мусора, или объекты, которые ещё не были обработаны сборщиком мусора.

При нормальной работе циклического сборщика мусора (cyclic gc) не должны встречаться объекты (находиться в списке одного из поколений сборщика мусора), у которых значение счётчика ссылок равно 0. Если же такие объекты попадают в сборщик мусора, значит tp_dealloc для объектов этого типа содержит ошибку — удаляемый объект не дерегистрируется из сборщика мусора и при этом происходит (явно или неявно) вызов в Python, который может привести к попытке повторного удаления обрабатываемого объекта. Почему будет попытка повторного удаления объекта — потому что объект всё ещё находится в списке gc (still‑tracked dying object), но при этом обратный вызов произошёл в середине выполнения tp_dealloc и объект находится в частично разрушенном состоянии.

После выполнения функции update_refs указатели _gc_prev содержат копии значений счётчика ссылок для объекта (я эти значения буду называть gc_refs ниже). И это также означает, что двусвязный список, в который объединены объекты, стал односвязным и в дальнейшем нам будет необходимо обратные связи восстановить.

Подсчёт входящих ссылок

Функция subtract_refs рассчитывает входящие ссылки для всех элементов переданного списка путём вызова функции tp_traverse, которой передаётся visit_decref и текущий объект в качестве аргумента arg.

static int
visit_decref(PyObject *op, void *parent)
{
    if (_PyObject_IS_GC(op)) {
        PyGC_Head *gc = AS_GC(op);
        if (gc_is_collecting(gc)) {
            gc_decref(gc);
        }
    }
    return 0;
}

static void
subtract_refs(PyGC_Head *containers)
{
    traverseproc traverse;
    PyGC_Head *gc = GC_NEXT(containers);
    for (; gc != containers; gc = GC_NEXT(gc)) {
        PyObject *op = FROM_GC(gc);
        traverse = Py_TYPE(op)->tp_traverse;
        (void) traverse(op, visit_decref, op);
    }
}

Отметим два момента. Во‑первых, функция tp_traverse определяется пользователем, и она должна вызвать переданную функцию для всех дочерних объектов, которые поддерживают сборку мусора. Если в этой функции для какого‑то из дочерних объектов такой вызов отсутствует, то это может приводить к утечкам памяти, так как данные объекты могут выступать внешними корнями, которые держат ссылки. Об этом чуть ниже.
Во‑вторых, функция обратного вызова visit_decref уменьшает значение счётчика ссылок (посредством вызова gc_decref), который хранится в gcprev и делает это только для объектов, находящихся в состоянии сборки мусора (установлен бит PREV_MASK_COLLECTING, проверяется через gc_is_collecting).

После вы��олнения функции subtract_refs объекты в списке имеют значение gc_refs больше или равно 0. Если значение gc_refs больше 0 — то такие объекты однозначно достижимы снаружи списка и сборщик мусора должен их пропустить. Что значит достижимы снаружи списка — например, мы собираем среднее поколение, а объект в старшем поколении ссылается на наш объект из среднего поколения. Или, как было сказано выше, мы допустили ошибку и пропустили какой‑то объект в tp_traverse, а значит, для такого объекта мы не вызовем gc_decref, а значит и значение будет выше положенного.

Проверка поддержки сборки мусора

Функция _PyObject_IS_GC проверяет, поддерживает ли тип объекта сборку мусора. Для этого должно выполниться два условия:

  1. Тип имеет установленный флаг Py_TPFLAGS_HAVE_GC

  2. И если слот tp_is_gc у типа объекта содержит ненулевой указатель на функцию, то эта функция должна вернуть true для проверяемого объекта

Разделение достижимых и недостижимых объектов

Как было сказано в предыдущем разделе, если значение gc_refs больше 0, то это значит, что объект достижим. Но если значение равно 0, то это ещё не значит, что объект недостижим. Чтобы разделить достижимые и недостижимые объекты существует функция move_unreachable(young, unreachable), которая определяет и перемещает недостижимые объекты из списка young в список unreachable.

static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *prev = young;
    PyGC_Head *gc = GC_NEXT(young);

    /* Record which old space we are in, and set NEXT_MASK_UNREACHABLE bit for convenience */
    uintptr_t flags = NEXT_MASK_UNREACHABLE | (gc->_gc_next & _PyGC_NEXT_MASK_OLD_SPACE_1);
    while (gc != young) {
        if (gc_get_refs(gc)) {
            PyObject *op = FROM_GC(gc);
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            (void) traverse(op,
                    visit_reachable,
                    (void *)young);
            _PyGCHead_SET_PREV(gc, prev);
            gc_clear_collecting(gc);
            prev = gc;
        }
        else {
            prev->_gc_next = gc->_gc_next;

            PyGC_Head *last = GC_PREV(unreachable);
            last->_gc_next = flags | (uintptr_t)gc;
            _PyGCHead_SET_PREV(gc, last);
            gc->_gc_next = flags | (uintptr_t)unreachable;
            unreachable->_gc_prev = (uintptr_t)gc;
        }
        gc = _PyGCHead_NEXT(prev);
    }
    young->_gc_prev = (uintptr_t)prev;
    young->_gc_next &= _PyGC_PREV_MASK;
    unreachable->_gc_next &= _PyGC_PREV_MASK;
}

Все объекты, которые останутся в списке young в том или ином виде достижимы снаружи начальной версии young и, наоборот, объекты в списке unreachable недостижимы снаружи начального списка young. У всех оставшихся объектов в списке young будет сброшен бит PREV_MASK_COLLECTING. И наоборот, у объектов в списке unreachable этот бит будет установлен.

Объекты в списке unreachable будут иметь установленную маску NEXT_MASK_UNREACHABLE и поэтому для этого списка не могут использоваться списочные функции. Эта маска может быть сброшена в этой функции, но этого не делается, чтобы избежать дополнительной итерации по списку. Маска будет сброшена при вызове одной из последующих функций в gc_collect_region.

Функция поддерживает следующие инварианты:

  1. Все объекты в списке young слева от текущего явно или неявно достижимы снаружи первоначального списка young.

  2. Недостижимые объекты слева от текущего перенесены в список unreachable и имеют установленную маску NEXT_MASK_UNREACHABLE.

  3. Все объекты слева от текущего в списке young были обработаны функцией, текущий и всё, что справа — ещё нет.

Для ускорения обработки, флаг, содержащий номер старшего поколения и установленную маску NEXT_MASK_UNREACHABLE, предрассчитывается до основного цикла следующим образом: flags = NEXT_MASK_UNREACHABLE | (gc->_gc_next & _PyGC_NEXT_MASK_OLD_SPACE_1) (_PyGC_NEXT_MASK_OLD_SPACE_1 — младший бит указателя на следующий объект).

Итак, после выполнения функций update_refs и subtract_refs список young содержит объекты, у которых значение временного счётчика ссылок либо больше 0, либо равно 0.

Возможно недостижимые объекты

Если значение временного счётчика ссылок равно 0, то объект возможно недостижим снаружи списка young. Почему возможно недостижим? Потому что он может быть дочерним объектом одного из достижимых объектов, которые находятся справа от текущего и ещё не проверены. В любом случае пока текущий объект должен быть перемещён в список недостижимых и у этого объекта должна быть установлена маска NEXT_MASK_UNREACHABLE.

Транзитивно достижимый объект

Если объект окажется транзитивно достижим, то visit_reachable удалит его из списка недостижимых, переместит его в конец списка young и он будет ещё раз обработан (подробнее в разделе про определение транзитивно достижимых объектов).

Прежде чем добавить его в список недостижимых, его надо удалить из списка young. gc->_gc_prev используется для хранения значения счётчика ссылок, а значит, список young является односвязным и удалить объект из списка можно следующим образом: prev->_gc_next = gc->_gc_next — указателю на следующий объект предыдущего объекта присваивается значение указателя на следующий объект текущего объекта (если при этом указатель имеет какие‑то дополнительные установленные биты, то они также будет скопированы).

Теперь текущий объект необходимо добавить в список недостижимых. Т.к. все объекты в списке недостижимых имеют установленную маску NEXT_MASK_UNREACHABLE, то списочные функции не могут быть использованы и мы должны реализовать аналог gc_list_append вручную. Алгоритм аналогичен тому, что описан в PyObject_GC_Track и здесь важно следующее:

  1. Заглавный объект списка не двигается — то есть список unreachable это буквально заглавный объект.

  2. last = unreachable->_gc_prev — предыдущий объект заглавного всегда указывает на последний объект в списке.

  3. Чтобы добавить объект в список, то нужно связать последний объект с добавляемым

    1. last->_gc_next = gc | flags — в указатель на следующий объект записываем указатель на текущий объект с установленной маской NEXT_MASK_UNREACHABLE и фиксированным номером старшего поколения.

    2. gc->_gc_prev = last — в указатель на предыдущий объект текущего записываем указатель на последний объект в списке.

  4. Теперь добавляемый объект надо связать с заглавным объектом:

    1. gc->_gc_next = unreachable | flags — указатель на следующий объект у текущего объекта должен указывать на заглавный, также с установленными служебными битами.

    2. unreachable->_gc_prev = gc — указатель на предыдущий объект у заглавного должен указывать на текущий.

  5. Т.к. мы добавляем объекты в конец списка, то указатель на следующий объект у заглавного никогда не меняется.

Заглавный объект

Указатель на следующий объект у заглавного объекта списка (как young, так и unreachable) не должен содержать установленную маску NEXT_MASK_UNREACHABLE. Но, так как в основном цикле мы устанавливаем эту маску безусловно, то можем испортить данный указатель. Поэтому после цикла у обоих списков, у заглавных объектов мы должны эту маску сбросить. Делается это следующим образом — list->_gc_next &= _PyGC_PREV_MASK — то есть младшие два бита сбрасываются.

Достижимые объекты

Если значение временного счётчика ссылок больше 0, то этот объект достижим снаружи изначального списка young. Все дочерние объекты проверяемого объекта также считаются достижимыми или транзитивно достижимыми (см. ниже определение транзитивно достижимых объектов). Транзитивно достижимыми объектами могут оказаться объекты, которые ранее были добавлены в список недостижимых — такие объекты будут обратно перемещены в конец списка young и повторно обработаны как достижимые.

Все достижимые объекты убираются из сборки мусора — у них сбрасывается бит PREV_MASK_COLLECTING (функция gc_clear_collecting). В частности, это необходимо для того, чтобы дважды не проверять такие объекты в visit_reachable.

Если текущий объект является последним в списке и он является достижимым, то visit_reachable может изменить список young, так как может вернуть транзитивно достижимые объекты обратно в список. Поэтому мы можем определить, какой объект будет обрабатываться следующим только после возврата из tp_traverse.

Двусвязные списки

Для хранения значения временного счётчика ссылок используется указатель на предыдущий объект _gc_prev и список young является односвязным списком. Функция move_unreachable должна восстановить двусвязность списка young.

Делается это следующим образом, при обработке достижимых объектов сохраняется указатель на предыдущий объект. И затем, после того как tp_traverse отработает, указатель _gc_prev может быть восстановлен путём вызова функции _PyGCHead_SET_PREV(gc, prev). А также путём установки указателя на предыдущий объект в соответствующий указатель у заглавного объекта списка young.

С этого момента список young восстанавливает двусвязность.

Список unreachable является двусвязным по построению, так как при добавлении объектов в список инициализируются указатели на предыдущий и следующий объекты.

Определение транзитивно достижимых объектов

Функция visit_reachable используется для обратного вызова из tp_traverse, для определения транзитивно достижимых объектов.

static int
visit_reachable(PyObject *op, void *arg)
{
    PyGC_Head *reachable = arg;
    if (!_PyObject_IS_GC(op)) {
        return 0;
    }

    PyGC_Head *gc = AS_GC(op);
    const Py_ssize_t gc_refs = gc_get_refs(gc);

    if (! gc_is_collecting(gc)) {
        return 0;
    }

    if (gc->_gc_next & NEXT_MASK_UNREACHABLE) {
        PyGC_Head *prev = GC_PREV(gc);
        PyGC_Head *next = GC_NEXT(gc);
        _PyObject_ASSERT(FROM_GC(prev),
                         prev->_gc_next & NEXT_MASK_UNREACHABLE);
        _PyObject_ASSERT(FROM_GC(next),
                         next->_gc_next & NEXT_MASK_UNREACHABLE);
        prev->_gc_next = gc->_gc_next;  // copy flag bits
        gc->_gc_next &= ~NEXT_MASK_UNREACHABLE;
        _PyGCHead_SET_PREV(next, prev);

        gc_list_append(gc, reachable);
        gc_set_refs(gc, 1);
    }
    else if (gc_refs == 0) {
        gc_set_refs(gc, 1);
    }
    else {
        _PyObject_ASSERT_WITH_MSG(op, gc_refs > 0, "refcount is too small");
    }
    return 0;
}

Вызывается из функции move_unreachable, при обходе объектов, у которых значение временного счётчика ссылок больше 0. Т.к. visit_reachable является делегатом для tp_traverse, то объекты, с которыми она работает, являются дочерними объектами тех, с которыми работает сборщик мусора в данный момент. Нас будут интересовать только те дочерние объекты, которые поддерживают сборку мусора (проверяется функцией _PyObject_IS_GC) и которые находятся в состоянии сборки мусора (проверяется функцией gc_is_collecting).

Если дочерние объекты не находятся в состоянии сборки мусора, то это либо объекты, которые находятся в другом поколении. Либо объекты, которые уже были обработаны и были признаны достижимыми (gc_refs > 0), �� этом случае move_unreachable сбрасывает бит PREV_MASK_COLLECTING (который проверяется функцией gc_is_collecting). Также можно сказать, что это объекты слева от текущего в списке young.

Итак, если дочерний объект находится в состоянии сборки мусора, то возможные следующие варианты: либо объект находится в списке недостижимых объектов, либо у него значение временного счётчика ссылок больше или равно 0.

Список недостижимых объектов

Т.к. объекты хранятся в двусвязных списках, то для проверки принадлежности объектов к тем или иным спискам может потребоваться большое количество операций.

Чтобы этого избежать используются помеченные (tagged) или толстые (fat) указатели (ранее мы с ними встречались). Поэтому для обозначения принадлежности объекта к списку недостижимых используется младший бит указателя на следующий объект.

Проверить наличие объекта в списке недостижимых можно следующим образом gc->_gc_next & NEXT_MASK_UNREACHABLE.

Итак, если объект находится в списке недостижимых, то это означает, что он ранее уже был обработан и значение временного счётчика ссылок у него равно 0. Но так как он попал в visit_reachable, то это означает, что он является дочерним объектом одного из достижимых объектов. В этом случае мы должны отметить его как достижимый объект. Для этого нужно сделать, так чтобы move_unreachable обработала его повторно, поэтому мы удалим его из списка недостижимых объектов, добавим в список young (visit_reachable вторым аргументом получает указатель на список young) и установим ему значение временного счётчика ссылок в 1 (можно в любое значение больше 0).

Т.к. все объекты в списке недостижимых имеют помеченные указатели, то мы не можем использовать списковые функции и должны вручную удалить объект из списка недостижимых. Делается это следующим образом:
1. prev->_gc_next = gc->_gc_next — следующий указатель предыдущего объекта указывает на следующий объект текущего и таким образом пропускает текущий в списке (при этом копируются помеченные указатели).
2. _PyGCHead_SET_PREV(next, prev) — предыдущий указатель следующего указателя указывает на предыдущий объект (с учётом флагов, которые хранятся в _gc_prev).

После того, как объект удалили из списка недостижимых, его надо добавить в список достижимых. Т.к. в списке достижимых не должно быть объектов с установленной маской NEXT_MASK_UNREACHABLE, то мы её сбрасываем и можем воспользоваться списочными функциями:
1. gc->_gc_next &= ~NEXT_MASK_UNREACHABLE — ~NEXT_MASK_UNREACHABLE — все биты, кроме самого младшего, равны 1.
2. gc_list_append(gc, young).

Второй тип объектов, с которыми мы можем столкнуться в функции visit_reachable — объекты, значение временного счётчика ссылок у которых больше или равно 0. Это объекты, которые находятся в списке young справа от текущего, и функция move_unreachable до них ещё не добралась. Т.к. эти объекты попали в visit_reachable, то аналогично предыдущему случаю, эти объекты транзитивно достижимы. И надо сделать так, чтобы move_unreachable обработала их соответствующим образом. Если значение временного счётчика ссылок больше 0, то делать ничего не надо. Если же значение равно 0, то нам достаточно искусственно задать значение временного счётчика ссылок. Подойдёт любое значение больше нуля, например, 1: gc_set_refs(gc, 1).

Почему перемещать недостижимые эффективнее

В комментариях к функции deduce_unreachable есть довольный объёмный комментарий, о том, что в текущей версии функция move_unreachable перемещает недостижимые объекты в список недостижимых, а достижимые оставляет в исходном списке.

Тут стоит отметить неочевидный момент — достижимые объекты, оставаясь в исходном списке, всё равно в нём перемещаются. В частности, меняется их первоначальный порядок, что имеет преимущество при последующем вызове сборщика мусора.

Представим, что у нас были созданы объекты A, B, C, в этом порядке. А связаны они, например, в следующим образом:

Первая сборка мусора после создания этих объектов, будет обрабатывать их в порядке A, B, C. Это приведёт к тому, что A и B будет признаны недостижимыми и перемещены в список unreachable. Но, так как на объект C есть внешняя ссылка, то он считается достижимым и все его дочерние объекты также являются достижимыми. А значит, объект B должен быть перемещён в список young. Когда move_reachable повторно дойдёт до обработки объекта B, то аналогично объект A будет перемещён в список young.

После первого выполнения функции move_unreachable, объекты A, B, C в исходном списке (в списке соответствующего поколения) будут располагаться в обратном порядке: C, B, A. При следующем вызове сборщика мусора объекты будут проверяться в этом же порядке и будут сразу после выполнения subtract_refs считаться достижимыми, а значит, не будут перемещаться в другие списки.

Таким образом, мы избавляемся от ненужных перемещений объектов при последующих запусках сборщика мусора, несмотря на то, что при первом запуске перемещение из young в unreachable и обратно кажется неэффективным.

Дерегистрация кортежей

Далее из исходного списка удаляются кортежи, которые соответствуют определённым условиям, см. untrack_tuples.

Объединение с целевым списком

Далее объекты из исходного переносятся в целевой. С оговорками. Во‑первых, если исходный и целевой списки совпадают, то перенос не выполняется, так как в нём отсутствует смысл. Исходный и целевой списки совпадают только для полной сборки мусора, так как в этом случае до вызова gc_collect_region молодое поколение и ожидающая часть старшего поколения переносятся в просмотренную часть. Во‑вторых, в случае сборки молодого поколения или инкрементальной сборки в этот момент исходный список становится пустым.

Важно отметить, что в этот момент список молодого поколения во всех трёх случаях становится пустым. Ниже постараюсь ответить, почему это важно.

После разделения исходного списка на достижимые и недостижимые дальнейшая работа идёт только со списком недостижимых объектов и целевым. В исходном списке остались достижимые, и нас они больше не интересуют.

Несобираемые объекты

Ранее до появления PEP 442 — Safe object finalization для финализации объектов использовался слот tp_del. Текущая версия сборщика мусора считает такие объекты небезопасными и не берёт их в работу. Все объекты, содержащие tp_del и все транзитивно из них достижимые переносятся в просмотренную часть старого поколения. Для работы с такими объектами используются три функции, которые рассмотрим ниже:

  1. move_legacy_finalizers

  2. move_legacy_finalizer_reachable

  3. handle_legacy_finalizers

Основная задача функции move_legacy_finalizers перенести объекты, которые имеют устаревший финализатор (has_legacy_finalizer проверяет, что указатель в tp_del ненулевой) в отдельный список — finalizers. Это объекты, которые считаются несобираемыми — uncollectable. При этом с помощью функции gc_clear_collecting сбрасывается маска PREV_MASK_COLLECTING, указывающая на то, что объект находится на этапе сборки мусора.

Замечание об оптимизации

Дополнительно к этому, у всех объектов в списке недостижимых сбрасывается маска NEXT_MASK_UNREACHABLE — это просто оптимизация, чтобы сэкономить лишний проход по списку в функции deduce_unreachable. Т.к. маска сброшена, то после вызова функции move_legacy_finalizers к списку недостижимых, а также списку несобираемых можно применять списочные функции.

Задача функции move_legacy_finalizer_reachable — найти все транзитивно достижимые объекты для объектов из списка несобираемых. Аналогично ранее разобранным функциям для обхода транзитивно достижимых объектов, используется слот tp_traverse с функцией visit_move. Выпрямление рекурсии происходит также аналогичным способом — через добавление новых объектов в список несобираемых. visit_move проверяет, что посещаемый объект находится в сборщике мусора (_PyObject_GC_IS_TRACKED) и находится в состоянии сборки (gc_is_collecting). Если объект условиям удовлетворяет, то он переносится в список несобираемых и для него также сбрасывается состояние сборки мусора (gc_clear_collecting).

Состояние сборки мусора

Почему важно проверять, что объект находится в состоянии сборки мусора, то есть у него выставлена маска PREV_MASK_COLLECTING? Т.к. мы используем tp_traverse с обратными вызовами, которые могут иметь произвольный код, в том числе, который совершает вызовы в Python или просто создаёт gc‑enabled объекты. Которые, в том числе могут попасть в транзитивно достижимые объекты (в списки недостижимых, несобираемых и тому подобное объектов).

Функция handle_legacy_finalizers вызывается под самый конец основной функции сборки мусора. Но забежим вперёд, чтобы описание работы с несобираемыми объектами не было разорвано.

Итак, handle_legacy_finalizers выполняет две задачи — а) переносит все объекты из списка несобираемых в просмотренную часть старшего поколения, б) объекты, которые имеют устаревший финализатор, добавляет в список gcstate->garbage. Если же установлен флаг _PyGC_DEBUG_SAVEALL, то все несобираемые объекты добавляются в список gcstate->garbage.

Отдельный список для несобираемых объектов

Если мы ничего не можем сделать с несобираемыми объектами, то зачем мы переносим эти объекты в старшее поколение, из которого они снова попадут в функцию сборки мусора при следующем вызове?

Короткий ответ — потому что они живые объекты. Они могут участвовать в циклах, они могут держать сильные ссылки на другие объекты и тому подобное Если при этом переместить их в отдельный список и, таким образом убрать из повторной обработки сборщиком мусора, то они будут выступать внешними корневыми объектами (external root object) и препятствовать сборке объектов, на которые отсутствуют другие ссылки. И в этом случае пришлось бы эти объекты всё равно брать в работу.

Почему несобираемые объекты переносятся в старшее поколение в конце сборки мусора? Потому что при обработке недостижимых объектов, некоторые несобираемые объекты могут быть удалены, так как существующие циклы будут разорваны. Очистка может произойти при вызове tp_clear у объекта, который хранит сильную ссылку на несобираемый объект. А раз объект будет удалён, то он будет удалён и из списка путём вызова PyObject_GC_UnTrack и, казалось бы, можно было бы обработку несобираемых объектов сгруппировать в самом начале главной функции сборки мусора. Но несобираемые объекты, которые имеют функцию tp_del, попадают также и в список gcstate->garbage, а оттуда PyObject_GC_UnTrack объект удалить не сможет.

Вышесказанное можно проиллюстрировать следующим примером (спасибо @grigorym за идею):

import gc

class A:
    def __del__(self):
        print("A.__del__")

class B:
    def __del__(self):
        print("B.__del__")

a = A()
b = B()

a.b = b
b.a = a

del a
del b

gc.collect()
print('garbage', gc.garbage)

Если запустить этот пример в Python 3.3, то мы получим следующий вывод:

('garbage', [<__main__.A instance at 0x000000000292C708>, <__main__.B instance at 0x000000000292C748>])

А если на более современном (от 3.4 и выше), то следующий:

A.__del__
B.__del__
garbage []

На этом с несобираемыми объектами всё, и мы можем вернуться к списку недостижимых объектов.

Обработка слабых ссылок с обратными вызовами

В списке недостижимых объектов могут быть как сами слабые ссылки, так и объекты, на которые имеются слабые ссылки.

static int
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
{
    PyGC_Head *gc;
    PyGC_Head wrcb_to_call;     /* weakrefs with callbacks to call */
    PyGC_Head *next;
    int num_freed = 0;

    gc_list_init(&wrcb_to_call);

    for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) {
        PyWeakReference **wrlist;
        PyObject *op = FROM_GC(gc);
        next = GC_NEXT(gc);

        if (! _PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
            continue;
        }

        wrlist = _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(op);

        PyWeakReference *next_wr;
        for (PyWeakReference *wr = *wrlist; wr != NULL; wr = next_wr) {
            next_wr = wr->wr_next;

            if (wr->wr_callback == NULL) {
                continue;
            }

            _PyWeakref_ClearRef(wr);
            if (gc_is_collecting(AS_GC((PyObject *)wr))) {
                continue;
            }

            Py_INCREF(wr);

            PyGC_Head *wrasgc = AS_GC((PyObject *)wr);
            gc_list_move(wrasgc, &wrcb_to_call);
        }
    }

    int visited_space = get_gc_state()->visited_space;
    while (! gc_list_is_empty(&wrcb_to_call)) {
        PyObject *temp;
        PyObject *callback;

        gc = (PyGC_Head*)wrcb_to_call._gc_next;
        PyObject *op = FROM_GC(gc);
        PyWeakReference *wr = (PyWeakReference *)op;
        callback = wr->wr_callback;

        temp = PyObject_CallOneArg(callback, (PyObject *)wr);
        if (temp == NULL) {
            PyErr_FormatUnraisable("Exception ignored on "
                                   "calling weakref callback %R", callback);
        }
        else {
            Py_DECREF(temp);
        }

        Py_DECREF(op);
        if (wrcb_to_call._gc_next == (uintptr_t)gc) {
            gc_set_old_space(gc, visited_space);
            gc_list_move(gc, old);
        }
        else {
            ++num_freed;
        }
    }

    return num_freed;
}

Слабые ссылки могут иметь обратные вызовы, которые могут использоваться для различных целей, но главное, что они могут запустить произвольный код на Python или C. В обратные вызовы передаётся слабая ссылка, через которую вызываемый код может создать сильную ссылку на объект.

Поэтому если слабые ссылки не будут очищены до вызова деструктора, то это может иметь неприятные эффекты, например, это может привести к материализации объектов, которые находятся в частично разрушенном состоянии (например, после вызова tp_finalize или tp_clear), за примером можно обратиться в GH-91636 — Assertion failure when func_repr is called on an already tp_clear‑ed object. В связи с этим, важно, чтобы слабые ссылки были очищены до вызова деструкторов, и так и было начиная с Python 2.3.

Но и тут не всё так просто.

У типов объектов есть специальный кеш. При обновлении кеша базового типа должны обновляться кеши у подклассов. Для этого используется список подклассов tp_subclasses. Что такое список подклассов — это словарь со слабыми ссылками на дочерние классы. И мы можем построить такой пример, в котором у нас будет очищаться список подклассов, а деструкторы этих классов будут обращаться к атрибутам базовых и дочерних объектов. И в этот момент будет происходить обращение к кешу, который не обновлён и содержит ссылки на удалённые объекты. Это краткое описание GH-135552 — Segmentation fault, possibly due to a GC issue (tp_subclasses). И как раз исследование этой ошибки привело меня к созданию этого списка статей.

Чтобы избавиться и от этой проблемы, было принято решение очищать слабые ссылки с обратными вызовами до вызова деструкторов, а остальные слабые ссылки после.

Для чего используется этот кеш

Python использует MRO — порядок разрешения методов, который используется для нахождения «правильной» версии атрибутов и методов в иерархии классов. Но так как иерархии могут быть довольно обширными, то поиск атрибутов и методов может занимать значительное время. С целью оптимизации результаты поиска кешируются в специальном словаре. При изменении атрибутов и методов необходимо кеш обновлять, чтобы все классы и объекты в иерархии использовали актуальные версии.

Итак, нам надо очистить слабые ссылки с обратными вызовами.

Список недостижимых объектов содержит объекты различных типов, некоторые из которых поддерживают создание слабых ссылок, а некоторые нет. Проверяется эта возможность функцией _PyType_SUPPORTS_WEAKREFS, при этом сами слабые ссылки не позволяют создавать ссылки на себя. Таким образом, в функции handle_weakref_callbacks мы работаем только с теми объектами, на которые су��ествуют слабые ссылки. Список слабых ссылок можно получить двумя способами — _PyObject_GET_WEAKREFS_LISTPTR и _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET. Последний вариант подходит только для динамических типов (для типов, у которых установлен флаг Py_TPFLAGS_HEAPTYPE, это в том числе типы, созданные в коде на Python) и чуть быстрее с точки зрения времени выполнения, чем первый вариант.

Статические типы не могут оказаться в списке недостижимых объектов — они обрабатываться специальным образом при финализации интерпретатора.

Функция _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSETвозвращает указатель на голову двусвязного списка слабых ссылок (в отличие от списков в GC этот список не кольцевой). Мы будем двигаться по этому списку и проверять, имеется ли у слабой ссылки указатель на функцию обратного вызова. Если обратного вызова у ссылки нет — то она пропускается, если же есть — то она очищается функцией _PyWeakref_ClearRef. Функция _PyWeakref_ClearRef удаляет объект из списка, а также в сам указатель на объект записывает Py_None. Это делается для того, чтобы можно было проверить, что объект, на который ссылается слабая ссылка, освобождён, а также как маркер, чтобы избежать повторного удаления слабой ссылки из списка.

Замечание о версии

Стоит заметить, что запись Py_None сделана только в версии 3.15 и ещё не перенесена в 3.14 и 3.13.

Если у слабой ссылки отсутствует обратный вызов, то функция _PyWeakref_ClearRefне будет вызвана и не обновит список, а значит, условия цикла не поменяются и мы можем зациклиться, проверяя один и тот же объект постоянно. Чтобы этого не происходило в самом начале цикла у каждой слабой ссылки берётся указатель на следующий элемент в списке, что и обеспечивает полную итерацию списка.

Если слабая ссылка имеет обратный вызов, всегда ли требуется его вызывать? Нет, не всегда. Если слабая ссылка, как и сам объект, находятся в списке недостижимых, то обратный вызов можно не совершать. Этот случай можно объяснить следующим образом: представим, что сначала очищается слабая ссылка, то есть на момент её очистки объект всё ещё существует, а значит, обратный вызов не должен происходить, а затем очищается и сам объект. Это проверяется условием gc_is_collecting(wr).

Если слабая ссылка достижима, то значит достижима и функция обратного вызова. В этом случае мы переносим слабую ссылку в промежуточный список, при этом увеличиваем значение её счётчика ссылок, чтобы гарантировать, что ссылка переживёт обратный вызов (что на самом деле необязательно, но добавляет детерминизма в процесс). Стоит заметить, что объекты не могут присутствовать в нескольких списках одновременно, и если мы переместили слабую ссылку в промежуточный список, то значит, удалили её из списка, в котором она находилась — в нашем случае это старшее поколение или список недостижимых объектов.

Следующим надо обработать промежуточный список, содержащий слабые ссылки с обратными вызовами. Для каждой слабой ссылки в списке необходимо вызвать функцию обратного вызова. Делается это следующим образом — PyObject_CallOneArg(callback, wr). Функция обратного вызова принимает один аргумент — саму слабую ссылку, это позволяет в функции обратного вызова получить ссылку на оригинальный объект и проверить, был ли он уничтожен или нет. Проверить это можно путём сравнения материализованной ссылки с Py_None — как мы видели выше, _PyWeakref_ClearRefзаписывает в указатель на оригинальный объект PyNone при очистке. Это также позволяет избежать нежелательного воскрешения объектов из функции обратного вызова (см. выше GH-91636).

После вызова функции нам необходимо уменьшить значение счётчика ссылок у слабой ссылки. В этот момент может произойти её очистка.

Если слабая ссылка осталась жива, то её надо переместить из промежуточного списка в просмотренную часть старшего поколения. Но мы не можем просто проверить указатель — если слабая ссылка была уничтожена, то указатель указывает на освобождённую память и обращение к нему приведёт к Segmentation fault в лучшем случае и к неопределённому поведению в худшем.

Чтобы убедиться, что слабая ссылка осталась жива, необходимо воспользоваться косвенным признаком. Если Py_DECREF(wr) удалил слабую ссылку, то для неё была вызвана функция PyObject_GC_UnTrack, которая удалила объект из списка, неважно из какого. Поэтому достаточно проверить, присутствует ли слабая ссылка, как и прежде, в промежуточном списке, то есть осталась ли она на своём месте или нет.

На этом с обработкой слабых ссылок с обратными вызовами мы закончили. Что стоит отметить:

  1. Если слабые ссылки были недостижимы на момент вызова handle_weakref_callbacks, то их обратные вызовы не будут совершены и сами ссылки останутся в тех списках, в которых они находятся.

  2. Если слабые ссылки были достижимы, то будут совершены соответствующие обратные вызовы, сами ссылки возможно будут удалены, а выжившие ссылки — перемещены в старшее поколение. Это также означает, что при последующем вызове сборки мусора (инкрементальной или полной сборки), эти ссылки могут повторно попасть в функцию handle_weakref_callbacks и для них повторно может быть совершён обратный вызов. Т.е. надо иметь в виду, что обратные вызовы должны быть идемпотенты.

Вызов деструкторов

Функция finalize_garbage вызывает деструкторы (tp_finalize или del) у объектов из списка недостижимых.

static void
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
{
    destructor finalize;
    PyGC_Head seen;
    gc_list_init(&seen);

    while (!gc_list_is_empty(collectable)) {
        PyGC_Head *gc = GC_NEXT(collectable);
        PyObject *op = FROM_GC(gc);
        gc_list_move(gc, &seen);
        if (!_PyGC_FINALIZED(op) &&
            (finalize = Py_TYPE(op)->tp_finalize) != NULL)
        {
            _PyGC_SET_FINALIZED(op);
            Py_INCREF(op);
            finalize(op);
            assert(!_PyErr_Occurred(tstate));
            Py_DECREF(op);
        }
    }
    gc_list_merge(&seen, collectable);
}

Вызов tp_finalize может привести к удалению не только текущего объекта, но и ряда других, которые также могут присутствовать в списке недостижимых. А это значит, что они при финализации могут из списка быть удалены. В такой ситуации, наиболее простым и надёжным способом обработки всех объектов в списке является следующий упрощённый алгоритм:

  1. Пока список не пуст,

  2. Взять первый объект списка,

  3. Перенести в дополнительный список,

  4. Вызвать деструктор.

Как было сказано выше, деструктор может привести к удалению объекта или ряда объектов, что, в свою очередь, приведёт к удалению этих объектов из любых списков, где бы они ни находились. Т.е. это означает, что вызов tp_finalize(op) может привести к удалению не только op из дополнительного списка, но и объектов, которые были ранее в этот список добавлены, или же удалены ещё не обработанные объекты из списка недостижимых.

Деструктор вызывается только для тех объектов, которые а) имеют ненулевой указатель в слоте tp_finalize, и б) сброшенную маску _PyGC_PREV_MASK_FINALIZED в указателе gcprev. Если эти условия выполнены, то устанавливается указанная выше маска, увеличивается значение счётчика ссылок и вызывается деструктор. Здесь значение счётчика ссылок увеличивается по той же причине, что и в handle_weakref_callbacks — мы хотим гарантировать, что объект не будет удалён во время вызова деструктора. После вызова мы уменьшаем значение счётчика ссылок, что может привести к удалению.

После окончания обработки списка недостижимых мы имеем следующий инвариант — список недостижимых пуст, а дополнительный список содержит объекты, которые пережили финализацию.

Чтобы продолжить работу оставшиеся объекты необходимо перенести обратно в список недостижимых.

Обработка воскрешённых объектов

Обе функции, handle_weakref_callbacks и finalize_garbage могут выполнять произвольный пользовательский код — это и обратные вызовы для слабых ссылок и собственные реализации деструкторов. При этом пользовательский код может приводить не только к удалению каких‑либо объектов (что также приведёт к удалению их из списков), но также и к появлению новых сильных ссылок на существующие объекты (например, пользовательский код может создать новую ссылку на объект, который находится в списке недостижимых) или создать новые объекты в молодом поколении.

С точки зрения выполняющейся процедуры сборки мусора нас будут интересовать только новые сильные ссылки, которые были созданы для объектов в списке недостижимых. К моменту выполнения функции handle_resurrected_objects часть объектов из оригинального списка недостижимых уже удалена при финализации. Оставшиеся объекты, а также их транзитивные зависимости необходимо проверить — являются ли они, как и прежде, недостижимыми или нет.

static inline void
handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable,
                           PyGC_Head *old_generation)
{
    gc_list_clear_collecting(unreachable);

    PyGC_Head* resurrected = unreachable;
    deduce_unreachable(resurrected, still_unreachable);
    clear_unreachable_mask(still_unreachable);

    gc_list_merge(resurrected, old_generation);
}

Все функции для проверки мы уже разбирали прежде — deduce_unreachable, разделит старый список недостижимых объектов на две части — на достижимые и недостижимые объекты. Но, так как эта функция использует маску PREV_MASK_COLLECTING для проверки, какие объекты уже были обработаны, то мы должны эту маску сбросить до вызова deduce_unreachable. Это делается функцией gc_list_clear_collecting.

После разделения достижимые объекты переносятся в просмотренную часть старшего поколения. Недостижимые объекты перенесены функцией deduce_unreachable в новый список final_unreachable. Для дальнейшей работы с этим списком необходимо у его объектов сбросить маску NEXT_MASK_UNREACHABLE. Но флаг PREV_MARK_COLLECTING будет у них установлен.

После выполнения функции handle_resurrected_objects старый список недостижимых объектов будет находиться в неопределённом состоянии и не должен более использоваться.

Очистка слабых ссылок

Ранее были очищены слабые ссылки, которые имеют обратные вызовы. Также мы говорили, что очистка всех слабых ссылок до вызова деструкторов может приводить к ошибкам времени выполнения. После финализации мы можем считать, что никакие объекты не воспользуются слабыми ссылками и можем их очистить.

Функция clear_weakref реализована аналогично handle_weakref_callbacks. За тем исключением, что список недостижимых объектов может содержать и слабые ссылки, которые также должны быть очищены.

Обработка циклов

Функция delete_garbage вызывает функцию tp_clear для всех объектов в списке недостижимых, у которых она определена.

static void
delete_garbage(PyThreadState *tstate, GCState *gcstate,
               PyGC_Head *collectable, PyGC_Head *old)
{
    while (!gc_list_is_empty(collectable)) {
        PyGC_Head *gc = GC_NEXT(collectable);
        PyObject *op = FROM_GC(gc);

        if (gcstate->debug & _PyGC_DEBUG_SAVEALL) {
            if (PyList_Append(gcstate->garbage, op) < 0) {
                _PyErr_Clear(tstate);
            }
        }
        else {
            inquiry clear;
            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                Py_INCREF(op);
                (void) clear(op);
                if (_PyErr_Occurred(tstate)) {
                    PyErr_FormatUnraisable("Exception ignored in tp_clear of %s",
                                           Py_TYPE(op)->tp_name);
                }
                Py_DECREF(op);
            }
        }
        if (GC_NEXT(collectable) == gc) {
            /* object is still alive, move it, it may die later */
            gc_clear_collecting(gc);
            gc_list_move(gc, old);
        }
    }
}

При обработке объектов в списке учитываются те же замечания, что и прежде.

Во‑первых, вызов функции tp_clear может привести к удалению объекта или ряда объектов из списка недостижимых. Но не только — могут быть удалены даже объекты из списка несобираемых, так как вызов tp_clear может привести к обнулению их значения счётчика ссылок. Поэтому проще всего итерироваться по списку, до тех пор, пока список не будет пуст.

Во‑вторых, чтобы гарантировать, что объект переживёт вызов tp_clear, до вызова этой функции, значение счётчика ссылок у объекта увеличивается (мы видели этот приём ранее).

В‑третьих, если объект остался жив, то он останется в списке недостижимых на том же месте, что и был (см. замечание в handle_weakref_callbacks). Если это так, то объект переносится в просмотренную часть старого поколения. Тут стоит сделать замечание, что если у объекта отсутствует функция tp_clear, то он автоматически будет перемещён в старшее поколение.

Если же включён режим отладки _PyGC_DEBUG_SAVEALL, то все объекты будут а) добавлены в список с мусором gcstate->garbage, и б) все они будут перемещены в просмотренную часть старшего поколения.

Также предполагается, что все объекты из списка недостижимых имеют значение счётчика ссылок больше нуля.

После вызова функции в списке недостижимых не должно остаться объектов, а выжившие объекты должны быть перемещены в старшее поколение.

Разница между tp_finalize и tp_clear

Функция, или корректнее слот, fp_finalize предназначена для выполнения финализации объекта. С точки зрения пользователя языка, это методы del, созданные у пользовательских объектов. Они могут выполнять какие‑то произвольные действия, связанные с фактом удаления объекта.

Функция tp_clear предназначена для очистки дочерних объектов, то есть она разрывает связи между двумя объектами. И с точки зрения сборки мусора, она может разорвать связь между объектами, которые образуют циклическую ссылку.

При реализации del также можно выполнить очистку зависимых объектов вручную, например следующим образом:

class C:
	def __init__(self):
		self.c = self
	def __del__(self):
		self.c = None
		# del self.c

С другой стороны, такая очистка может быть выполнена автоматически, так как дочерние объекты хранятся в словаре главного объекта. Это позволяет избежать ошибок, которые могут возникнуть при ручной очистке.

С точки зрения разработчиков расширений для CPython, ситуация несколько различается, так как зачастую в целях оптимизации, не все дочерние объекты размещаются в словаре объекта. В этом случае ошибка, допущенная при реализации tp_clear будет приводить к утечкам памяти.

Также очистку можно реализовать и в tp_finalize.

Ещё одно отличие, связано с тем, что мы рассмотрели ранее — функция финализации tp_finalize защищена флагом _PyGC_PREV_MASK_FINALIZED и вызывается для объекта только один раз. Тогда как tp_clear может вызываться несколько раз, если объект несколько раз попадает в старшее поколение.

За вызовом delete_garbage следует вызов функции handle_legacy_finalizers, которую мы рассмотрели ранее.

И заканчивают главную функцию сборки мусора проверки, что просмотренная часть старшего поколения находится в консистентном состоянии:

  1. Все объекты находятся в просмотренной части.

  2. У всех объектов сброшены маски PREV_MASK_COLLECTING и NEXT_MASK_UNREACHABLE.

Заключение

В этой части мы рассмотрели главную функцию сборщика мусора. Ключевыми моментами являются определение недостижимых объектов, обработка слабых ссылок с обратными вызовами и обработка циклов.

На этом я закончу цикл статей о том, как устроена сборка мусора в Python для версии с включённым GIL. Сборщик мусора для Python с выключенным GIL (или free threading), основывается на версии с тремя поколениями, но требует отдельного разбора, так как подсчёт ссылок реализован по‑другому, а это влияет на реализацию сборщика мусора.

Также в стороне остался разбор того, как устроена финализация интерпретатора. Т.к. основным механизмом автоматического управления памятью в Python является подсчёт ссылок, то важным является порядок финализации модулей и подсистем во время завершения работы интерпретатора.

Спасибо за внимание.