Как сборщик мусора Python обрабатывает циклические ссылки между объектами
Что проверяют
Знаете ли вы, что в CPython два механизма, и понимаете ли, как именно находится цикл. Следом спрашивают, как искать утечку, если память всё равно растёт.
Как отвечать
Основа это подсчёт ссылок: счётчик лежит в заголовке объекта, sys.getrefcount() его показывает (плюс временная ссылка от самого вызова). При обнулении объект освобождается сразу, и __del__ срабатывает предсказуемо. Цикл, например родитель со списком детей, у каждого из которых ссылка на родителя, до нуля не дойдёт.
Для циклов есть отдельный сборщик:
- кого обходит: только контейнеры с
tp_traverse, то есть списки, словари, экземпляры классов. Проверить объект можно черезgc.is_tracked(); - алгоритм: счётчики набора контейнеров копируются в служебное поле, затем каждая ссылка изнутри набора уменьшает копию. Ненулевой остаток значит внешнюю ссылку: объект и всё достижимое от него живы. Остальное недостижимо и удаляется, до этого обрабатываются слабые ссылки и финализаторы;
- поколения: молодые объекты проверяются чаще, пережившие сборку переходят в старшее поколение. Запуск привязан к разнице между выделениями и освобождениями контейнеров, пороги читает
gc.get_threshold()и меняетgc.set_threshold(); - финализаторы: начиная с 3.4 (PEP 442) циклы с
__del__собираются, а не оседают вgc.garbage.
Практика: разрывать естественные циклы через weakref, например обратную ссылку на родителя или WeakValueDictionary для кеша. Для поиска утечек подходят tracemalloc со сравнением снимков, gc.get_referrers() и objgraph. В prefork-серверах gc.freeze() (3.7+) до fork() переносит объекты в постоянное поколение, и сборщик не ломает copy-on-write.
Где спотыкаются
Описывают классический mark-and-sweep от глобальных переменных и стека. CPython не знает всех корней, ссылки держат и C-расширения, поэтому он вычитает внутренние ссылки.
Лечат утечку вызовом gc.collect(). Если объект достижим из живого кеша или глобального списка, сборщик его правильно не трогает.
Отключают gc ради скорости и забывают, что код с циклами, например ORM-модели с обратными связями или сохранённые исключения с traceback, начнёт копить память.