PythonMiddleTechnical
Зачем нужен garbage collector, если есть reference counting?
Reference counting не освобождает циклы: если A ссылается на B, а B на A, refcount каждого = 1, удалить нельзя. Cyclic GC периодически ищет недостижимые из корней группы container-объектов с внутренними ссылками и освобождает их (gc-модуль, generational).
Где не справляется reference counting
CPython управляет памятью двумя механизмами:
- Reference counting — у каждого PyObject есть поле
ob_refcnt.Py_INCREF/Py_DECREFправят счётчик при создании и удалении ссылок. Когда счётчик падает до 0, объект освобождается немедленно. - 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 по поколениям:
- Сборщик берёт все объекты текущего поколения, копирует их
gc_refs=ob_refcnt. - Идёт по ссылкам между ними и вычитает 1 у целей: после прохода
gc_refsотражает только внешние ссылки. - Объекты с
gc_refs == 0— потенциально мусорные. Транзитивно проверяются достижимые из них. - Недостижимые освобождаются. Финализаторы (
__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.