Как CPython обнаруживает и освобождает недостижимый цикл ссылок, который не удаляется одним подсчётом ссылок?
Подсчёт ссылок не может освободить объекты, которые ссылаются друг на друга, даже если из программы к ним больше нет пути. Поэтому CPython дополнительно использует циклический сборщик мусора: он периодически анализирует отслеживаемые контейнеры, находит недостижимые группы объектов и разрывает их внутренние ссылки, после чего объекты освобождаются обычным механизмом управления памятью.
Подсчёт ссылок удобен тем, что большинство объектов освобождается сразу после исчезновения последней ссылки. Однако этот механизм сам по себе не решает проблему циклов: взаимные ссылки сохраняют ненулевые счётчики даже при полной недостижимости объектов.
Циклический сборщик мусора был добавлен как дополнительный механизм, а не как замена подсчёту ссылок. Такой подход сохраняет предсказуемое немедленное освобождение для обычных объектов и покрывает отдельный случай циклических зависимостей.
Цикл может возникнуть в графе объектов не только явно, но и через контейнеры, обратные ссылки, кэши, обработчики событий или двусвязные структуры. После удаления внешней ссылки такой граф остаётся в памяти, пока сборщик циклов не докажет, что ни один объект внутри него больше недостижим из корней приложения.
Если циклы создаются часто, а циклический сборщик отключён, отложен или запускается слишком редко, растёт объём занятой Python-памяти и увеличивается давление на память. Если запускать его слишком часто, возрастает стоимость обхода графа объектов и возможны дополнительные паузы.
Счётчик ссылок каждого объекта учитывает входящие ссылки. Для цикла этого недостаточно: каждый объект внутри цикла продолжает иметь хотя бы одну ссылку от другого объекта того же цикла.
Циклический сборщик отслеживает прежде всего контейнеры, способные содержать ссылки на другие объекты. При сборке он строит логическую картину внутренних ссылок и отделяет ссылки, приходящие извне, от ссылок между объектами-кандидатами. Если группа не имеет достижимых внешних ссылок, она считается недостижимой.
Затем сборщик разрывает ссылки внутри такой группы. Это уменьшает счётчики ссылок её членов до нуля или делает объекты доступными для дальнейшего освобождения. Сборщик работает периодически, используя поколения и пороги запуска; точное расписание зависит от настроек и версии Python.
Минимальная демонстрация:
В строке перед gc.collect() перед вызовом не должно быть лишнего отступа; корректный вариант — вызвать gc.collect() без начального пробела. До явного запуска цикла внешних ссылок уже нет, но отключённый автоматический сборщик его не обработал. После запуска сборки слабая ссылка очищается, потому что объект стал недостижимым.
Сборщик циклов не обязан отслеживать каждый объект: простые неизменяемые значения обычно не могут образовать цикл сами по себе, а отслеживание сосредоточено на потенциально циклических контейнерах. При этом пользовательские экземпляры часто участвуют в отслеживаемых графах через свои атрибуты.
Объекты с финализаторами, например с методом __del__, требуют особой осторожности. Современный CPython умеет финализировать многие циклы с такими объектами, но повторное создание внешней доступности объекта внутри финализатора, то есть resurrection, может изменить результат сборки. Поэтому финализаторы усложняют анализ времени освобождения и порядка очистки.
Настройки модуля gc позволяют вручную запускать сборку, менять пороги и временно отключать автоматический механизм. Это инструмент диагностики или точечной оптимизации, а не универсальный способ ускорения программы: изменение порогов следует подтверждать измерениями профилирования.
В сервисе обработки событий после удаления подписчиков память постепенно увеличивалась. Причиной оказалась двусторонняя связь: подписчик хранил ссылку на диспетчер, а диспетчер — список подписчиков. Удаление подписчика из внешнего реестра не разрушало уже созданные циклы.
Рассматривались три варианта. Полностью отключить циклический сборщик было рискованно: это могло уменьшить паузы сборки, но привело бы к накоплению циклов. Принудительно вызывать gc.collect() после каждой операции было проще для проверки, но создавало лишние дорогостоящие обходы графа. Переписать структуру владения с использованием слабых ссылок или явного метода отписки было сложнее, зато уменьшало саму вероятность образования цикла.
Выбрали устранение обратной сильной ссылки там, где диспетчер не должен владеть подписчиком, и оставили автоматический сборщик включённым. Это снизило количество циклов и необходимость в принудительных сборках, сохранив корректность очистки для остальных объектов.
1. Все ли объекты внутри цикла отслеживаются циклическим сборщиком?
Нет. Сборщик ориентирован на объекты-контейнеры, способные хранить ссылки и участвовать в циклах. Простые атомарные объекты, такие как некоторые числа и строки, сами по себе не образуют циклов, поэтому их отдельное отслеживание не требуется. Важно рассуждать не о наличии цикла как абстрактного факта, а о том, какие объекты в графе могут содержать ссылки на другие объекты.
2. Что изменится, если участник цикла определяет __del__?
Финализатор делает время и порядок очистки менее очевидными. Благодаря современному протоколу финализации многие циклы с __del__ могут быть обработаны, но финализатор способен воскресить объект, сохранив на него ссылку; тогда объект или часть цикла перестаёт быть недостижимой. Поэтому на __del__ не следует опираться как на точный механизм управления ресурсами; для файлов, соединений и блокировок предпочтительнее явное закрытие или контекстный менеджер.
3. Почему отключение циклического сборщика иногда ускоряет программу, но не является универсальной оптимизацией?
Пока сборщик отключён, программа не тратит время на его периодические обходы, что может уменьшить накладные расходы в сценариях без циклов. Но созданные циклы при этом не освобождаются, даже если их обычные внешние ссылки исчезли, поэтому занятая память накапливается. Отключение допустимо только для ограниченного участка с доказанным отсутствием циклов и после проверки времени работы и потребления памяти; обычно безопаснее настроить пороги или устранить сами циклические связи.