Опишите судьбу обычного объекта в CPython после исчезновения последней сильной ссылки?
В CPython у обычного объекта при достижении счётчиком сильных ссылок нуля немедленно запускается его освобождение: вызывается финализация, освобождаются его внутренние ресурсы, а память передаётся аллокатору CPython. Это происходит без ожидания очередного запуска сборщика циклов.
Правило не распространяется напрямую на объекты, входящие в циклы ссылок: у них счётчик может оставаться ненулевым, хотя объект уже недостижим. Такие циклы дополнительно обрабатывает циклический сборщик мусора.
Подсчёт ссылок появился как основной механизм управления памятью CPython, чтобы освобождать большинство объектов предсказуемо и без постоянного полного обхода всей кучи. Это особенно удобно для ресурсов, связанных с объектами: файлов, соединений, блокировок и буферов.
Цена такой модели — необходимость обновлять счётчик при создании и удалении каждой ссылки. Кроме того, одного подсчёта ссылок недостаточно для циклических структур, поэтому в CPython он дополнен сборщиком циклов.
Неверно считать, что оператор del обязательно уничтожает объект. Он удаляет конкретную связь между именем и объектом; объект будет освобождён только после исчезновения всех сильных ссылок на него.
Практическая ошибка возникает, когда разработчик ожидает немедленного возврата памяти операционной системе. Освобождение объекта и уменьшение RSS процесса — разные события: память может остаться во внутренних пулах CPython и быть повторно использована только самим процессом.
Каждая сильная ссылка на объект увеличивает его счётчик ссылок, а удаление ссылки уменьшает его. Когда счётчик достигает нуля, CPython может немедленно вызвать деаллокацию объекта. Для объектов с пользовательским __del__ это также означает вызов финализатора, но полагаться на него для критически важных ресурсов не следует.
Ссылки могут находиться в локальных переменных, контейнерах, атрибутах, глобальных структурах, замыканиях и временных выражениях. Поэтому удаление одного имени часто ничего не меняет: другие ссылки продолжают удерживать тот же объект.
После del obj объект сохраняется через alias. После del alias в обычном CPython его счётчик ссылок становится нулевым, поэтому сообщение о финализации обычно печатается сразу. Это демонстрация механизма CPython, а не универсальная гарантия языка Python.
Цикл ссылок работает иначе: объекты внутри него продолжают ссылаться друг на друга, поэтому их счётчики не равны нулю. Если извне цикл больше недостижим, его обнаруживает и освобождает сборщик циклов; момент освобождения в этом случае не обязан быть немедленным.
Для расширений на C уменьшение счётчика обычно выполняется через операции уровня Py_DECREF. Ошибка в управлении такими ссылками может привести к преждевременному освобождению, утечке или повреждению памяти. Кроме того, некоторые специальные объекты CPython могут иметь особый жизненный цикл, поэтому правило следует формулировать именно для обычных объектов.
В обработчике большого файла разработчик читает блок, обрабатывает его и удаляет локальную переменную, ожидая пропорционального снижения RSS. Внутри функции это может действительно освободить объект сразу, но RSS останется высоким, если память удерживается другим контейнером, трассировщиком, буфером или внутренним аллокатором CPython.
Вариант с принудительным вызовом gc.collect() не решает проблему обычных ссылок: он предназначен прежде всего для поиска циклов и добавляет задержку. Вариант с сохранением всех блоков в списке гарантированно продлевает их жизнь и увеличивает пик памяти.
Предпочтительное решение — проверить реальные сильные ссылки и время жизни объектов, затем отдельно измерить Python-аллокации через tracemalloc и RSS процесса через системный инструмент. После устранения удерживающей ссылки объекты будут освобождаться по подсчёту ссылок, а оставшаяся память может быть безопасно переиспользована последующими аллокациями процесса.
1. Освобождается ли память операционной системы сразу после достижения нулевого счётчика?
Нет. Деаллокация объекта возвращает память внутреннему аллокатору Python или соответствующему пулу. Аллокатор может сохранить её внутри процесса для будущих объектов, поэтому RSS не обязан уменьшиться.
2. Почему цикл ссылок не освобождается одним подсчётом ссылок?
Взаимные ссылки поддерживают ненулевые счётчики объектов даже после потери внешних ссылок. Специальный сборщик анализирует достижимость таких объектов и удаляет недостижимые циклы, поэтому освобождение происходит позже и с дополнительными затратами.
3. Гарантирует ли язык Python немедленное уничтожение объекта после исчезновения последней ссылки?
Нет. Это характерная деталь реализации CPython, где используется подсчёт ссылок. Другие реализации Python могут применять отложенный или трассирующий сборщик, поэтому код не должен зависеть от момента вызова __del__; для ресурсов нужно использовать явное закрытие или контекстный менеджер.