CPython и сборка мусора
Создал объект и посмотрел на счётчик ссылок: 1. Присвоил во вторую переменную - стало 2. Удалил вторую - снова 1. Удалил последнюю - объект освободился НЕМЕДЛЕННО, деструктор отработал сразу. Потом сделал два объекта, которые ссылаются друг на друга, и удалил обе переменные: не освободилось НИЧЕГО. И только явный вызов сборщика убрал оба.
Стержень: основной механизм - подсчёт ссылок, он мгновенный; отдельный сборщик нужен только для циклов, которые подсчёту не по зубам.
// Формулировки: «как в Python работает сборка мусора?», «зачем нужен gc, если есть подсчёт ссылок?», «что такое поколения?»
Подсчёт ссылок
У каждого объекта есть счётчик: сколько мест на него ссылается. Присвоение в переменную, добавление в список, передача в функцию - счётчик растёт. Выход из области видимости, удаление, перезапись - падает. Как только счётчик дошёл до нуля, память освобождается тут же, в этот же момент.
Мой замер показывает это по шагам: 1, потом 2 после второй переменной, потом снова 1, а на нуле сработал деструктор. Отсюда важное свойство: в Python не бывает ситуации «мусор накопился, надо подождать сборщика» для обычных объектов. Файл, закрытый в деструкторе, закроется сразу, а не когда-нибудь.
// Плата за это - работа на каждой операции с объектом и невозможность параллельного счёта в потоках без глобальной блокировки: именно подсчёт ссылок надо было бы защищать замками на каждый объект. Ещё один побочный эффект: счётчик занимает место в каждом объекте, поэтому даже пустой список весит 56 байт, а не ноль.
- счётчик ссылок
- сколько мест ссылается на объект; на нуле память освобождается сразу
- деструктор
- метод, который вызывается при освобождении объекта
Циклы и сборщик
Подсчёт ссылок ломается на одном случае - когда объекты ссылаются друг на друга. Я поставил такой опыт: два объекта, у каждого ссылка на соседа. Удалил обе внешние переменные - счётчики остались по единице, память не освободилась. Оба объекта живы и недостижимы.
Для этого и существует отдельный сборщик: он периодически обходит объекты-контейнеры и находит группы, которые ссылаются только друг на друга. В моём замере явный запуск сборщика освободил оба объекта и отчитался: собрано 2.
// Работает он по поколениям: новые объекты в нулевом, пережившие сборку переезжают в первое, потом во второе. Пороги на моей версии - 700, 10, 10: нулевое поколение собирается, когда разница между созданными и удалёнными объектами перевалит за 700, первое - каждые десять сборок нулевого, и так далее. Логика простая: большинство объектов умирает молодыми, поэтому часто проверять надо только их.
- цикл ссылок
- объекты ссылаются друг на друга; счётчики не доходят до нуля
- поколения
- молодые объекты проверяются часто, старые редко
Что с этим делать на практике
Первое: не полагаться на деструктор для важных вещей. Он сработает своевременно у обычного объекта, но у объекта в цикле - только после сборщика, а при выходе из программы может не сработать вовсе. Файлы, соединения и замки закрывают явно, через конструкцию с автоматическим освобождением.
Второе: знать про слабые ссылки. Они позволяют ссылаться на объект, НЕ увеличивая счётчик, - это стандартный способ разорвать цикл в схемах вроде «родитель знает детей, дети знают родителя».
// Третье: помнить, что сборщик стоит времени. В сервисах с большим количеством долгоживущих объектов (кэши, загруженные справочники) он регулярно обходит их впустую. Приёмы известны: увеличить пороги, а в крайних случаях выключить автоматический сбор и запускать его вручную в спокойные моменты. Но делать это стоит только после замера, а не заранее.
- слабая ссылка
- ссылка, не увеличивающая счётчик; помогает разорвать цикл
- явное освобождение
- конструкция with вместо надежды на деструктор
Как отвечать: «Как в Python освобождается память?»
Основной механизм - подсчёт ссылок. У каждого объекта есть счётчик, и как только он доходит до нуля, память освобождается немедленно, в этот же момент. Я это проверял по шагам: счётчик рос при новой переменной, падал при удалении, и на нуле деструктор сработал сразу. Поэтому в Python нет привычного по другим языкам ожидания сборщика для обычных объектов. Но у подсчёта есть слепое пятно - циклы. Я делал два объекта, ссылающихся друг на друга: удалил обе внешние переменные, и не освободилось ничего, потому что у каждого осталась ссылка от соседа. Для этого и нужен отдельный сборщик: он обходит контейнеры и находит недостижимые группы. Работает он по поколениям, у меня пороги были 700, 10, 10 - молодые объекты проверяются часто, старые редко, потому что большинство объектов умирает молодыми.
Ответ разделяет два механизма, показывает слепое пятно первого на воспроизведённом примере и объясняет логику поколений. Это точная механика, а не «в питоне есть сборщик мусора».
На чём валятся
- −Говорят «в Python сборщик мусора» и не знают про подсчёт ссылок как основной механизм.
- −Полагаются на деструктор для закрытия файлов и соединений.
- −Не знают, что циклы освобождаются только отдельным сборщиком.
- −Крутят пороги сборщика без замера, наугад.
- −Держат ссылки на объекты в глобальных списках и удивляются росту памяти.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Зачем в CPython нужен отдельный модуль gc поверх подсчёта ссылок?A)Дефрагментировать кучу и перемещать объекты для уплотнения памятиB)Ускорять доступ к часто используемым объектам за счёт кэшаC)Заменять подсчёт ссылок целиком на трассирующий сборщик мусораD)Собирать недостижимые циклические ссылки, поколенчески
показать ответ и разбор
+D)Собирать недостижимые циклические ссылки, поколенчески// разбор: Циклический сборщик (gc) находит группы объектов, ссылающихся друг на друга, но недостижимых извне, и освобождает их — то, что refcount пропускает. Он поколенческий (три поколения): молодые объекты проверяются чаще. Refcounting остаётся основой; gc — дополнение именно под циклы.
sys.getrefcount(obj)для только что созданного объекта показывает на 1 больше ожидаемого. Почему?A)CPython держит скрытую системную ссылку на каждый объектB)getrefcount округляет счётчик вверх для потокобезопасности чтенияC)Объект автоматически кэшируется, и кэш добавляет вторую ссылкуD)Сам аргумент функции — ещё одна временная ссылкапоказать ответ и разбор
+D)Сам аргумент функции — ещё одна временная ссылка// разбор: Чтобы передать объект в getrefcount, интерпретатор создаёт ещё одну ссылку — параметр функции. Поэтому результат на единицу больше «настоящего» числа ссылок в вашем коде. Отсюда типичная 2 для свежего объекта под одним именем. Это нюанс измерения, а не утечка.
- Как CPython в первую очередь освобождает память объектов?A)Подсчётом ссылок: как только их число падает до нуля, объект уничтожаетсяB)Ручным освобождением: объект живёт, пока разработчик явно не вызовет его удалениеC)Перемещением живых объектов в новую область, как это делают компактящие сборщикиD)Периодическим сборщиком, который раз в интервал обходит всю кучу и чистит мусор
показать ответ и разбор
+A)Подсчётом ссылок: как только их число падает до нуля, объект уничтожается// разбор: Основной механизм памяти в CPython — подсчёт ссылок: у каждого объекта счётчик, растущий при новой ссылке и убывающий при её исчезновении; достиг нуля — объект немедленно освобождается. Это детерминированно и быстро для типичного кода. Отдельный циклический сборщик нужен лишь там, где refcounting бессилен, — при ссылочных циклах.
- Зачем CPython отдельный циклический сборщик мусора вдобавок к подсчёту ссылок?A)Чтобы ускорить программу, периодически освобождая вообще все неиспользуемые объекты сразуB)Ловить ссылочные циклы: они держат счётчик > 0 и сами не освободятсяC)Чтобы компактить кучу и устранять фрагментацию памяти между живыми объектамиD)Чтобы вручную управлять моментом освобождения каждого создаваемого объекта в коде
показать ответ и разбор
+B)Ловить ссылочные циклы: они держат счётчик > 0 и сами не освободятся// разбор: Объекты, ссылающиеся друг на друга (a.ref=b, b.ref=a), держат счётчики ссылок положительными даже когда снаружи на них никто не смотрит — refcounting их не освободит, будет утечка. Циклический сборщик периодически находит такие недостижимые группы и разрывает их. Он дополняет подсчёт ссылок, а не заменяет: обычные объекты освобождаются сразу по refcount.
- Почему сборщик мусора CPython поколенческий (generational)?A)Большинство объектов живут недолго — молодые проверяют чаще, старые режеB)Чтобы делить объекты по размеру: крупные проверяются чаще мелких при каждом проходеC)Чтобы каждому потоку выделить своё поколение объектов и убрать конкуренцию за памятьD)Чтобы хранить объекты разных типов в разных поколениях для ускорения доступа к ним
показать ответ и разбор
+A)Большинство объектов живут недолго — молодые проверяют чаще, старые реже// разбор: В основе — «слабая генерационная гипотеза»: большинство объектов умирают молодыми, а пережившие несколько сборок живут долго. Поэтому объекты делят на поколения: молодое проверяют часто (там много мусора и проверка дешёвая), пережившие переводят в старшие поколения, которые сканируют всё реже. Это снижает объём работы сборщика без потери полноты.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.