PythonMiddleTechnical

Зачем нужен garbage collector, если есть reference counting?

Reference counting не освобождает циклы: если A ссылается на B, а B на A, refcount каждого = 1, удалить нельзя. Cyclic GC периодически ищет недостижимые из корней группы container-объектов с внутренними ссылками и освобождает их (gc-модуль, generational).

Где не справляется reference counting

CPython управляет памятью двумя механизмами:

  1. Reference counting — у каждого PyObject есть поле ob_refcnt. Py_INCREF/Py_DECREF правят счётчик при создании и удалении ссылок. Когда счётчик падает до 0, объект освобождается немедленно.
  2. Cyclic garbage collector (gc-модуль) — отдельный сборщик для container-объектов (list, dict, set, instance-классы, frame, ...).

Refcount не справляется с циклами: два объекта ссылаются друг на друга, внешние ссылки удалены, но ob_refcnt у обоих = 1. Они никогда не освободятся без отдельного сборщика.

Демонстрация цикла

import gc, sys

class Node:
    pass

a = Node()
b = Node()
a.partner = b           # a -> b
b.partner = a           # b -> a

print(sys.getrefcount(a))     # 3: a, b.partner, аргумент getrefcount
del a, b                       # внешних имён нет, но цикл остался в куче

# Без gc.collect() они зависли бы до следующего автозапуска
collected = gc.collect()
print("freed:", collected)     # > 0

Как работает cyclic GC

Алгоритм mark-and-sweep по поколениям:

  1. Сборщик берёт все объекты текущего поколения, копирует их gc_refs = ob_refcnt.
  2. Идёт по ссылкам между ними и вычитает 1 у целей: после прохода gc_refs отражает только внешние ссылки.
  3. Объекты с gc_refs == 0 — потенциально мусорные. Транзитивно проверяются достижимые из них.
  4. Недостижимые освобождаются. Финализаторы (__del__) вызываются по особым правилам.

Только контейнеры отслеживаются GC. int, str, tuple из immutable-элементов в циклах не участвуют и не попадают под сбор.

API gc-модуля

import gc
gc.get_threshold()    # (700, 10, 10) — пороги поколений
gc.get_count()        # текущие счётчики
gc.collect(2)         # принудительный полный сбор
gc.disable()          # отключить автоматический сбор
gc.set_debug(gc.DEBUG_LEAK)   # лог утечек
gc.get_objects()      # все известные сборщику объекты
gc.get_referrers(obj) # кто держит ссылку на obj — для поиска утечек

Подводные камни

  • Циклы с __del__ и финализаторами раньше не собирались; с 3.4 (PEP 442) уже собираются, но порядок финализации непредсказуем.
  • Внешние ресурсы (файл, сокет, lock) в цикле — освободятся когда-то, а до этого ресурс утечёт. Всегда with/close(), не полагайтесь на GC.
  • Тяжёлые кеши, удерживающие миллион объектов, — это утечка через ссылки, не цикл; GC её не починит. Используйте weakref.
  • Отключение GC в hot-path может ускорить, но требует gc.collect() вручную; иначе RSS будет расти.
  • Долгий gc.collect() с большой кучей даёт паузы — наблюдается в Django/Celery с большими объектами.

Common mistakes

  • Не объяснять циклические ссылки.
  • Говорить, что refcount и GC — одно и то же.
  • Забывать про недетерминированность cyclic GC.

What the interviewer is testing

  • Показывает цикл ссылок.
  • Понимает роль gc.collect.
  • Знает, почему with лучше del.

Sources

Related topics