Память и сборка мусора
В Python нет доступа к объекту напрямую — имя, элемент списка или атрибут это ссылка, а сам объект живёт в куче. Поэтому управление памятью здесь сводится к двум вопросам: сколько ссылок на объект ещё существует и достижим ли он снаружи вообще. CPython отвечает на них двумя независимыми механизмами, и почти все ошибки в теме — это смешение их зон ответственности. Первый, подсчёт ссылок, работает непрерывно и освобождает объект в тот же момент, когда исчезла последняя ссылка на него. Второй, сборщик циклов из модуля gc, запускается периодически и закрывает ровно один случай, недоступный первому, — взаимные ссылки.
Есть и третий слой, о котором вспоминают реже всего, — аллокатор. Разрушение объекта не означает, что память ушла операционной системе: pymalloc удерживает освобождённые блоки в своих пулах и отдаёт системе только полностью опустевшую арену. Отсюда самый дорогой миф темы — «RSS процесса не падает, значит, есть утечка». Не значит. И ещё одна оговорка, которую на senior-собеседовании ждут отдельно: всё перечисленное — свойства CPython, а не языка. Спецификация не обещает ни подсчёта ссылок, ни момента разрушения объекта; в PyPy подсчёта ссылок нет вообще, и код, полагающийся на «файл закроется сам, как только имя выйдет из области видимости», там молча ломается.
Карта темы
- Подсчёт ссылок — счётчик в заголовке каждого объекта, каскадное освобождение на нуле и почему освобождённая память при этом остаётся за процессом.
- Поверхностная и глубокая копия — присваивание не копирует ничего,
copy.copyкопирует один уровень,copy.deepcopyобходит весь граф с защитой от циклов. - Сборщик циклов — почему подсчёт ссылок слеп к взаимным ссылкам и как
gcдоказывает недостижимость вычитанием внутренних ссылок. - Поколения и настройка
gc— три поколения и смысл порога 700,gc.disable()иgc.collect()на массовой загрузке,gc.freeze()передfork.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что память освобождает только периодический сборщик | Пропущен основной механизм — подсчёт ссылок освобождает объект немедленно, а gc занимается лишь циклами |
Верить числу из sys.getrefcount буквально | Оно всегда на единицу больше из-за самого аргумента, а у None, True и малых int в CPython 3.12+ счётчик «бессмертный» и вообще не меняется |
Ждать, что del x уменьшит потребление памяти процессом | del убирает только имя; блок объекта возвращается в пул pymalloc, а не операционной системе |
| Приравнивать нерастущий обратно RSS к утечке | Арена отдаётся ОС только целиком пустой — гораздо чаще это фрагментация, а не утечка, и различать их надо по числу живых объектов |
Считать, что original[:] или list(original) дают независимую копию | Это ровно поверхностная копия — вложенные объекты общие, и мутация вложенного видна в обоих |
Думать, что gc.disable() останавливает освобождение памяти | Останавливается только сборщик циклов; подсчёт ссылок не отключается и отключён быть не может |
| Считать, что старшие поколения сканируются чаще младших | Наоборот — чем старше поколение, тем реже проход, ведь выживший объект статистически будет жить долго |
Измерять размер структуры через sys.getsizeof | Функция поверхностна и учитывает только сам объект — список из двух больших вложенных списков покажет 72 байта |
Значение для собеседований
Тему разворачивают лесенкой, и первая ступень почти всегда одна и та же — «как в Python освобождается память». Правильный ответ начинается со слов «сначала подсчёт ссылок, и он освобождает сразу», а ответ «периодически приходит сборщик мусора» немедленно превращает беседу в разбор ошибок. Дальше идёт почти гарантированная пара copy и deepcopy — часто в виде практической задачи, где копию сняли перед изменением, а оригинал всё равно изменился. Здесь ждут не заученного определения, а объяснения через ссылки: внешний контейнер новый, вложенные объекты те же самые.
На middle и senior вопросы становятся механическими. Зачем нужен gc, если есть подсчёт ссылок — ответ про циклы. Как сборщик отличает недостижимый цикл от живого — ответ про вычитание внутренних ссылок. Что именно считает порог поколения 0 — не время и не число объектов в куче, а разницу между созданными и уничтоженными отслеживаемыми контейнерами. Можно ли что-то отключить — подсчёт ссылок нельзя, сборщик циклов можно и иногда нужно. И финальный senior-вопрос про RSS, который не возвращается системе. Типичная ошибка на всём этом блоке — назвать gc единственным механизмом либо объявить любой рост памяти утечкой; сильный ответ, наоборот, сам оговаривает версию CPython, потому что пороги и размеры арен — деталь реализации, а не гарантия языка.