Управление памятью и сборка мусора
Как CPython выделяет, отслеживает и освобождает объекты во время выполнения
- объяснить, как подсчёт ссылок определяет момент освобождения объекта и почему
sys.getrefcountвсегда показывает на единицу больше; - описать, как циклические ссылки обходят чистый подсчёт ссылок и как поколенческий сборщик их освобождает;
- использовать
weakref, чтобы наблюдать за объектом или кэшировать его, не удерживая его в живых; - избегать классических ошибок
__del__(воскрешение, неопределённый порядок в циклах, завершение работы); - диагностировать неожиданный рост памяти с помощью
tracemalloc,gcиsys.getsizeof.
Эта тема предполагает, что вы уже знаете объектную модель Python — идентичность, изменяемость и семантику копирования из предыдущей темы. Здесь мы спускаемся на уровень глубже, к машинерии времени выполнения, которая удерживает эти объекты в живых.
1. Подсчёт ссылок: основной механизм
Каждый создаваемый CPython объект несёт небольшое целочисленное поле ob_refcnt, которое считает, сколько ссылок сейчас на него указывает. Привязка имени, добавление в список, передача аргумента — каждое из этих действий прибавляет единицу к счётчику; удаление имени, его переприсваивание или выход из области видимости вычитают единицу. Правило точное и мгновенное: как только счётчик достигает нуля, объект освобождается, запускается его __del__ (если он есть), и освобождаются его собственные ссылки — что может каскадом освободить ещё больше объектов.
import sys
a = [10, 20, 30] # у объекта-списка теперь одна ссылка: 'a'
b = a # вторая ссылка на ТОТ ЖЕ объект
print(sys.getrefcount(a)) # 3 (a, b и собственный аргумент getrefcount)
del b # одна ссылка исчезла
print(sys.getrefcount(a)) # 2 (a и аргумент)
del a # последняя реальная ссылка исчезла -> счётчик -> 0 -> освобождён сейчас
Именно поэтому CPython освобождает большую часть памяти детерминированно и быстро, без паузы «остановки мира»: освобождение вплетено в обычный учёт ссылок. Поэтому же файл, закрытый менеджером контекста, освобождается сразу, как только блок with завершается и исчезает его последняя ссылка.