сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Рантайм Python

CPython и сборка мусора

Как освобождается память

Создал объект и посмотрел на счётчик ссылок: 1. Присвоил во вторую переменную - стало 2. Удалил вторую - снова 1. Удалил последнюю - объект освободился НЕМЕДЛЕННО, деструктор отработал сразу. Потом сделал два объекта, которые ссылаются друг на друга, и удалил обе переменные: не освободилось НИЧЕГО. И только явный вызов сборщика убрал оба.

Стержень: основной механизм - подсчёт ссылок, он мгновенный; отдельный сборщик нужен только для циклов, которые подсчёту не по зубам.

// Формулировки: «как в Python работает сборка мусора?», «зачем нужен gc, если есть подсчёт ссылок?», «что такое поколения?»

Подсчёт ссылок

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

Мой замер показывает это по шагам: 1, потом 2 после второй переменной, потом снова 1, а на нуле сработал деструктор. Отсюда важное свойство: в Python не бывает ситуации «мусор накопился, надо подождать сборщика» для обычных объектов. Файл, закрытый в деструкторе, закроется сразу, а не когда-нибудь.

// Плата за это - работа на каждой операции с объектом и невозможность параллельного счёта в потоках без глобальной блокировки: именно подсчёт ссылок надо было бы защищать замками на каждый объект. Ещё один побочный эффект: счётчик занимает место в каждом объекте, поэтому даже пустой список весит 56 байт, а не ноль.

счётчик ссылок
сколько мест ссылается на объект; на нуле память освобождается сразу
деструктор
метод, который вызывается при освобождении объекта

Циклы и сборщик

Подсчёт ссылок ломается на одном случае - когда объекты ссылаются друг на друга. Я поставил такой опыт: два объекта, у каждого ссылка на соседа. Удалил обе внешние переменные - счётчики остались по единице, память не освободилась. Оба объекта живы и недостижимы.

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

// Работает он по поколениям: новые объекты в нулевом, пережившие сборку переезжают в первое, потом во второе. Пороги на моей версии - 700, 10, 10: нулевое поколение собирается, когда разница между созданными и удалёнными объектами перевалит за 700, первое - каждые десять сборок нулевого, и так далее. Логика простая: большинство объектов умирает молодыми, поэтому часто проверять надо только их.

цикл ссылок
объекты ссылаются друг на друга; счётчики не доходят до нуля
поколения
молодые объекты проверяются часто, старые редко

Что с этим делать на практике

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

Второе: знать про слабые ссылки. Они позволяют ссылаться на объект, НЕ увеличивая счётчик, - это стандартный способ разорвать цикл в схемах вроде «родитель знает детей, дети знают родителя».

// Третье: помнить, что сборщик стоит времени. В сервисах с большим количеством долгоживущих объектов (кэши, загруженные справочники) он регулярно обходит их впустую. Приёмы известны: увеличить пороги, а в крайних случаях выключить автоматический сбор и запускать его вручную в спокойные моменты. Но делать это стоит только после замера, а не заранее.

слабая ссылка
ссылка, не увеличивающая счётчик; помогает разорвать цикл
явное освобождение
конструкция with вместо надежды на деструктор

Как отвечать: «Как в Python освобождается память?»

Основной механизм - подсчёт ссылок. У каждого объекта есть счётчик, и как только он доходит до нуля, память освобождается немедленно, в этот же момент. Я это проверял по шагам: счётчик рос при новой переменной, падал при удалении, и на нуле деструктор сработал сразу. Поэтому в Python нет привычного по другим языкам ожидания сборщика для обычных объектов. Но у подсчёта есть слепое пятно - циклы. Я делал два объекта, ссылающихся друг на друга: удалил обе внешние переменные, и не освободилось ничего, потому что у каждого осталась ссылка от соседа. Для этого и нужен отдельный сборщик: он обходит контейнеры и находит недостижимые группы. Работает он по поколениям, у меня пороги были 700, 10, 10 - молодые объекты проверяются часто, старые редко, потому что большинство объектов умирает молодыми.

Ответ разделяет два механизма, показывает слепое пятно первого на воспроизведённом примере и объясняет логику поколений. Это точная механика, а не «в питоне есть сборщик мусора».

На чём валятся

  • Говорят «в Python сборщик мусора» и не знают про подсчёт ссылок как основной механизм.
  • Полагаются на деструктор для закрытия файлов и соединений.
  • Не знают, что циклы освобождаются только отдельным сборщиком.
  • Крутят пороги сборщика без замера, наугад.
  • Держат ссылки на объекты в глобальных списках и удивляются росту памяти.

Проверьте себя

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

  1. #cpython_gc1 / 5
    Зачем в CPython нужен отдельный модуль gc поверх подсчёта ссылок?
    A)Дефрагментировать кучу и перемещать объекты для уплотнения памяти
    B)Ускорять доступ к часто используемым объектам за счёт кэша
    C)Заменять подсчёт ссылок целиком на трассирующий сборщик мусора
    D)Собирать недостижимые циклические ссылки, поколенчески
    показать ответ и разбор
    +D)Собирать недостижимые циклические ссылки, поколенчески

    // разбор: Циклический сборщик (gc) находит группы объектов, ссылающихся друг на друга, но недостижимых извне, и освобождает их — то, что refcount пропускает. Он поколенческий (три поколения): молодые объекты проверяются чаще. Refcounting остаётся основой; gc — дополнение именно под циклы.

  2. #cpython_gc2 / 5
    sys.getrefcount(obj) для только что созданного объекта показывает на 1 больше ожидаемого. Почему?
    A)CPython держит скрытую системную ссылку на каждый объект
    B)getrefcount округляет счётчик вверх для потокобезопасности чтения
    C)Объект автоматически кэшируется, и кэш добавляет вторую ссылку
    D)Сам аргумент функции — ещё одна временная ссылка
    показать ответ и разбор
    +D)Сам аргумент функции — ещё одна временная ссылка

    // разбор: Чтобы передать объект в getrefcount, интерпретатор создаёт ещё одну ссылку — параметр функции. Поэтому результат на единицу больше «настоящего» числа ссылок в вашем коде. Отсюда типичная 2 для свежего объекта под одним именем. Это нюанс измерения, а не утечка.

  3. #cpython_gc3 / 5
    Как CPython в первую очередь освобождает память объектов?
    A)Подсчётом ссылок: как только их число падает до нуля, объект уничтожается
    B)Ручным освобождением: объект живёт, пока разработчик явно не вызовет его удаление
    C)Перемещением живых объектов в новую область, как это делают компактящие сборщики
    D)Периодическим сборщиком, который раз в интервал обходит всю кучу и чистит мусор
    показать ответ и разбор
    +A)Подсчётом ссылок: как только их число падает до нуля, объект уничтожается

    // разбор: Основной механизм памяти в CPython — подсчёт ссылок: у каждого объекта счётчик, растущий при новой ссылке и убывающий при её исчезновении; достиг нуля — объект немедленно освобождается. Это детерминированно и быстро для типичного кода. Отдельный циклический сборщик нужен лишь там, где refcounting бессилен, — при ссылочных циклах.

  4. #cpython_gc4 / 5
    Зачем CPython отдельный циклический сборщик мусора вдобавок к подсчёту ссылок?
    A)Чтобы ускорить программу, периодически освобождая вообще все неиспользуемые объекты сразу
    B)Ловить ссылочные циклы: они держат счётчик > 0 и сами не освободятся
    C)Чтобы компактить кучу и устранять фрагментацию памяти между живыми объектами
    D)Чтобы вручную управлять моментом освобождения каждого создаваемого объекта в коде
    показать ответ и разбор
    +B)Ловить ссылочные циклы: они держат счётчик > 0 и сами не освободятся

    // разбор: Объекты, ссылающиеся друг на друга (a.ref=b, b.ref=a), держат счётчики ссылок положительными даже когда снаружи на них никто не смотрит — refcounting их не освободит, будет утечка. Циклический сборщик периодически находит такие недостижимые группы и разрывает их. Он дополняет подсчёт ссылок, а не заменяет: обычные объекты освобождаются сразу по refcount.

  5. #cpython_gc5 / 5
    Почему сборщик мусора CPython поколенческий (generational)?
    A)Большинство объектов живут недолго — молодые проверяют чаще, старые реже
    B)Чтобы делить объекты по размеру: крупные проверяются чаще мелких при каждом проходе
    C)Чтобы каждому потоку выделить своё поколение объектов и убрать конкуренцию за память
    D)Чтобы хранить объекты разных типов в разных поколениях для ускорения доступа к ним
    показать ответ и разбор
    +A)Большинство объектов живут недолго — молодые проверяют чаще, старые реже

    // разбор: В основе — «слабая генерационная гипотеза»: большинство объектов умирают молодыми, а пережившие несколько сборок живут долго. Поэтому объекты делят на поколения: молодое проверяют часто (там много мусора и проверка дешёвая), пережившие переводят в старшие поколения, которые сканируют всё реже. Это снижает объём работы сборщика без потери полноты.

дальше

Теорию прочитали. Навык ставится повторением

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