После удаления переменной большой объект остаётся в памяти: какой механизм нужно проверить первым?

После удаления переменной большой объект остаётся в памяти: какой механизм нужно проверить первым?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Первым нужно проверить наличие других сильных ссылок на объект. Удаление переменной удаляет только её имя из пространства имён; сам объект освобождается лишь тогда, когда на него больше нет сильных ссылок и он не удерживается циклом или специальным объектом.

В CPython обычно срабатывает подсчёт ссылок: удаление последней ссылки немедленно уменьшает счётчик до нуля и запускает освобождение объекта. Однако это не гарантирует немедленного уменьшения RSS процесса, потому что освобождённая память может остаться у аллокатора Python или системного аллокатора для повторного использования.

Исторический контекст

Подсчёт ссылок стал базовым механизмом управления временем жизни объектов в CPython, поскольку он позволяет освобождать большинство объектов сразу после исчезновения последней ссылки. Это уменьшает необходимость периодически останавливать программу для полной сборки мусора.

Одного подсчёта ссылок недостаточно: два объекта могут ссылаться друг на друга, оставаясь недостижимыми извне, но имея ненулевые счётчики. Поэтому в CPython дополнительно работает сборщик циклического мусора, который обнаруживает такие циклы.

Постановка проблемы

Удаление локального имени часто ошибочно принимают за удаление самого объекта. Например, функция может вернуть объект, передать его в список, сохранить в кэше или оставить на него ссылку в замыкании; после этого удаление локальной переменной ничего не меняет в его времени жизни.

Неверный вывод особенно опасен при обработке больших буферов, результатов запросов или изображений. Разработчик может бесконечно вызывать удаление переменных, хотя объект продолжает удерживаться другой структурой данных.

Подробное решение

Имя переменной — это ссылка на объект, а не контейнер, владеющий объектом независимо от остальных ссылок. Когда имя удаляется, уменьшается число ссылок на объект, но объект продолжает существовать, если на него ссылаются другие переменные, контейнеры, атрибуты, замыкания, кэши или фреймы исключений.

buffer = bytearray(10_000_000) alias = buffer del buffer print(len(alias)) # Объект всё ещё доступен del alias # В CPython исчезает последняя ссылка

В CPython при исчезновении последней обычной ссылки счётчик ссылок становится нулевым, после чего вызывается освобождение объекта. Для объектов, участвующих в цикле ссылок, этот путь не срабатывает; их может освободить циклический сборщик мусора.

Важно различать освобождение объекта и возврат памяти операционной системе. Даже после уничтожения объекта память может остаться в пулах CPython или в аллокаторе процесса и использоваться будущими объектами. Поэтому для диагностики сначала проверяют удерживающие ссылки и размер живых объектов, а не только RSS.

В других реализациях Python детали отличаются: наличие и момент работы подсчёта ссылок не следует считать универсальным свойством языка. Код должен полагаться на достижимость объектов и корректное управление ссылками, а не на гарантии немедленного освобождения.

Ситуация из практики

В пакетном обработчике после обработки каждого крупного блока разработчик удаляет локальную переменную, но память продолжает расти. Проверка показывает, что обработанные блоки добавляются в список для журнала, а затем этот список передаётся в объект мониторинга и живёт дольше самой операции.

Рассматривались три варианта: принудительно запускать сборщик мусора, копировать данные перед обработкой или убрать ненужное сохранение блоков. Принудительный сборщик не помог бы с обычными сильными ссылками, копирование увеличило бы пик памяти, поэтому выбрали удаление лишнего списка и хранение только агрегированных метаданных.

После этого блоки стали освобождаться при исчезновении последней ссылки. RSS при этом мог уменьшиться не сразу, но последующие партии перестали накапливать живые объекты, что подтвердили сравнением снимков памяти и количества элементов в удерживающих структурах.

Что кандидаты часто упускают

  1. Всегда ли исчезновение последней ссылки немедленно освобождает объект?

В CPython для большинства объектов с нулевым счётчиком ссылок освобождение обычно происходит немедленно. Исключения связаны, например, с объектами, образующими циклы, с финализаторами и с особенностями конкретной реализации Python. Кроме того, освобождение объекта не означает обязательного возврата выделенной памяти операционной системе.

  1. Может ли контейнер удерживать объект, если он явно не используется в программе?

Да. Список, словарь, кэш, очередь, глобальная структура или атрибут другого объекта удерживают свои элементы сильными ссылками, даже если логика приложения больше не обращается к ним. Для поиска причины нужно исследовать владельцев объекта, а не только места, где он создаётся.

  1. Почему циклическая ссылка отличается от обычного дополнительного псевдонима?

При обычном псевдониме объект всё ещё может быть освобождён, когда исчезает последняя внешняя ссылка. В цикле объекты ссылаются друг на друга, поэтому их счётчики не становятся нулевыми после удаления внешнего имени. Циклический сборщик анализирует достижимость таких объектов и может удалить недостижимый цикл, если этому не препятствуют финализаторы или другие специальные условия.