Что такое reference counting?
Reference counting — механизм управления памятью в CPython: у каждого PyObject поле ob_refcnt. Каждое создание ссылки делает Py_INCREF, каждое удаление — Py_DECREF. При ob_refcnt == 0 объект освобождается немедленно (вызывается __del__, потом tp_dealloc). Не справляется с циклами — для них есть cyclic GC.
Идея и устройство
CPython управляет временем жизни объектов через подсчёт ссылок. Каждый PyObject имеет поле ob_refcnt (целое число). Когда создаётся новая ссылка (присваивание, добавление в список, передача в функцию) — счётчик растёт. Когда ссылка исчезает (выход из scope, del, перепривязка имени) — падает. Когда падает до 0, объект освобождается немедленно.
Эта модель отличается от трасинг-GC (Java, Go): не нужны паузы для маркировки, объекты освобождаются предсказуемо и сразу.
Что увеличивает refcount
import sys
obj = {"name": "cache"} # refcount = 1
print(sys.getrefcount(obj)) # 2 (наша ссылка + временная в getrefcount)
# +1 при добавлении в контейнер
refs = [obj]
print(sys.getrefcount(obj)) # 3
# +1 при передаче в функцию (на время вызова)
def take(x):
print(sys.getrefcount(x)) # 4 — наша, контейнер, x в функции, временная
take(obj)
# del уменьшает только эту ссылку
del refs
print(sys.getrefcount(obj)) # 2
Что del делает и не делает
del name — удаляет имя name из текущего scope. Это уменьшает refcount объекта на 1; если других ссылок нет — объект освобождается, иначе живёт дальше.
a = [1, 2, 3]
b = a # refcount = 2
del a # del уменьшил до 1, объект жив через b
print(b) # [1, 2, 3]
Когда вызывается __del__
При освобождении объекта Python вызывает __del__ (если есть), потом tp_dealloc, потом освобождает память. Важно: __del__ ≠ deconstructor C++. Он не гарантирует мгновенный вызов в случаях циклов или прерванного интерпретатора.
Проблема циклов
class Node:
pass
a = Node(); b = Node()
a.partner = b
b.partner = a
print(sys.getrefcount(a)) # 3
del a, b # внешних имён нет — но в куче по ссылке друг на друга
# refcount у обоих остался = 1
# Освободит их только cyclic GC: gc.collect()
Cyclic GC (mark-and-sweep по поколениям) — отдельный механизм для container-объектов, см. py-gc-needed-065.
Производительность и стоимость
- Очень частые
Py_INCREF/Py_DECREF— основной overhead CPython, ~10% времени выполнения. - Branch на
--ob_refcnt == 0в каждом DECREF. - Atomics для thread-safe-refcount при отсутствии GIL (PEP 703 — free-threaded CPython 3.13) дороже обычных операций.
Малоизвестные нюансы
- Immortal objects (PEP 683, 3.12+): синглтоны вроде
None,True,False, маленькие int имеют specially-large refcount и не освобождаются никогда. sys.getrefcount(x)возвращает значение, которое включает временную ссылку от самого вызова — учитывайте.- При завершении интерпретатора некоторые объекты могут не пройти
__del__— порядок неопределён.
Подводные камни
- Полагаться на
__del__для освобождения ресурсов — для циклов сработает поздно, для shutdown может вообще не сработать. Используйтеwithи контекст-менеджеры. - Считать
sys.getrefcount(x)точным production-инструментом — добавляет временную ссылку, и в многопоточном коде неустойчив. - Не понимать, что
delудаляет имя, а не объект. - Большие циклические структуры (двусвязный список) — без cyclic GC будут расти; в hot path можно делать
weakref. - Free-threaded CPython 3.13 меняет схему: biased reference counting, и поведение sys.getrefcount под нагрузкой иное.
- Утечка в C-extension через
Py_INCREFбезPy_DECREFприводит к росту памяти, который не виден из gc-модуля.
Common mistakes
- Путать имя и объект.
- Не упоминать CPython.
- Не связывать refcount с GIL и памятью.
What the interviewer is testing
- Объясняет увеличение/уменьшение refcount.
- Знает ограничение циклов.
- Понимает del как удаление ссылки.