Сборка мусора: почему объект жив, хотя ссылок на него нет
В обычной сборке CPython объект умирает в тот момент, когда на него пропадает последняя ссылка, — сборщик для этого не нужен. Сборщик нужен для циклов, и только для них. А объект, который «не умирает», может держать не мусорный цикл, а ссылка от живого объекта, которой не видно в коде: трассировка исключения, ключ кеша, метод в списке подписчиков.
Полное техническое изложение
TL;DR
В обычной сборке CPython объект умирает, когда пропадает последняя ссылка на
него, — сразу, на этой же строке. Этим занимается счётчик ссылок, а не сборщик мусора. del
не уничтожает объект, а убирает имя и уменьшает счётчик на единицу. Сборщик
(gc) нужен ровно для одного случая, который счётчик не умеет: цикл.
Две вершины ссылаются друг на друга, имён больше нет, а счётчики остаются по
единице — и объекты живут до gc.collect(), который находит группу без
внешних ссылок и освобождает её.
Дальше — то, что отличает знающего от читавшего. Когда память «не
освобождается», искать стоит не только мусорный цикл, но и ссылку от живого
объекта, которой не видно в коде. Исключение, сохранённое в атрибут, держит трассировку, трассировка —
кадр упавшей функции, кадр — все его локальные переменные. lru_cache на
методе держит self в ключе. Метод, положенный в список подписчиков, держит
свой объект в __self__. Всё это достижимо от живого объекта, и сборщик этого
не тронет — gc.collect() в прогоне ничего не менял. Зато одна правка рвёт
путь, и объект умирает на той же строке. А за полную сборку
платят не мусором, а живыми объектами: миллион живых списков — 39,88 мс на
gc.collect() без единого мусорного объекта (3.13.7).
С 3.13 у CPython две сборки, и всё сказанное выше — про обычную, с GIL. В
сборке без GIL (3.13t, 3.14t) экземпляр, созданный и отпущенный одним потоком,
тоже умирает на строке del. Но функция уровня модуля после del ждёт
сборщика, объект, последнюю ссылку на который отпустил чужой поток, эту строку
переживает, а gc.freeze() снимает со сборки не всё.
- что переменная — это имя, привязанное к объекту;
- что объект можно положить в список, в словарь, в атрибут другого объекта;
- как работают
try/exceptи что такое метод экземпляра.
- счётчик ссылок,
gc.collect(), недостижимые циклы,weakref; - трассировка и кадр в исключении,
lru_cacheна методе,WeakMethod, PEP 442,gc.freeze().
База: объект умирает, когда пропадает последняя ссылка
У каждого объекта в CPython есть счётчик: сколько ссылок на него сейчас существует. Имя — ссылка. Элемент списка — ссылка. Атрибут другого объекта — ссылка. Когда ссылка появляется, счётчик растёт, когда пропадает — уменьшается. Дошёл до нуля — объект освобождается сразу.
Так устроена обычная сборка CPython — та, что ставится по умолчанию. С 3.13 есть и вторая, без GIL; у правила там есть исключения, они разобраны в конце урока. Ответу уровня Junior они не мешают: для объекта, который создаёт и отпускает один поток, правило верно в обеих сборках.
Отсюда ответ уровня Junior на вопрос «как в Python освобождается память»: основной механизм — подсчёт ссылок: объект освобождается, как только на него не остаётся ни одной ссылки. Сборщик мусора дополняет его и нужен для циклов.
И сразу следствие, на котором спотыкаются: del x не удаляет объект. Он
удаляет имя x и уменьшает счётчик на единицу. Если это была последняя
ссылка — объект умрёт. Если нет — будет жить дальше.
Проверить «жив ли объект» можно, не удерживая его, — слабой ссылкой из модуля
weakref. Она не увеличивает счётчик, а после смерти объекта возвращает
None. Этим приёмом сделаны все прогоны урока.
Механизм 1: цикл — единственное, чего счётчик не умеет
Без цикла всё происходит на строке del:
1) без цикла
до del: жив
сразу после del: мёртв — освобождён счётчиком ссылок, сборщик не нужен
Теперь две вершины, которые ссылаются друг на друга:
a, b = Node("a"), Node("b")
a.other, b.other = b, a
del a, b2) цикл из двух вершин
sys.getrefcount(a) до del: 3
после del a, b: жив и жив
gc.collect() вернул 2; теперь: мёртв и мёртв
sys.getrefcount показывает 3: имя a, ссылку из b.other и собственный
аргумент функции — третью единицу добавляет сам вызов. После
del имён нет, но у каждой вершины осталось по ссылке — от соседа. Счётчик не
опустится до нуля никогда: из программы до вершин не добраться, а друг друга
они держат.
Такую группу и ищет сборщик циклов. Документация описывает его роль одной фразой:
the collector supplements the reference counting already used in Python
(сборщик дополняет подсчёт ссылок, который в Python уже есть).
Он обходит отслеживаемые объекты (контейнеры — списки, словари, экземпляры
классов; числа и строки он не отслеживает, у них не бывает ссылок на другие
объекты) и находит группы, на которые нет ссылок извне. gc.collect()
возвращает число найденных недостижимых объектов — здесь 2. Запускается он и
сам, по порогам числа созданных объектов; как устроены эти пороги и поколения,
разобрано в статье об управлении памятью.
Механизм 2: объект жив не из-за цикла, а из-за ссылки, которой не видно
На вопрос «почему память не освобождается» первым приходит в голову «где-то цикл». Но объект может держать и обычная ссылка от живого объекта — вполне законно, просто её нет в коде на виду. Ниже три такие ссылки.
Трассировка исключения держит кадр
У исключения есть атрибут __traceback__. Трассировка держит кадр каждой
функции, через которую прошло исключение, а кадр — все её локальные
переменные. Справочник языка называет это прямо, объясняя, почему имя из
except ... as e исчезает после блока:
Exceptions are cleared because with the traceback attached to them, they form a
reference cycle with the stack frame, keeping all locals in that frame alive
until the next garbage collection occurs.
Исключения удаляются, потому что вместе с прикреплённой трассировкой они образуют цикл ссылок с кадром стека, и все локальные переменные этого кадра остаются живыми до следующей сборки мусора.
Прогон проверяет три случая: исключение нигде не сохранено; сохранено в локальную переменную той же функции; сохранено в атрибут живого объекта.
3) исключение и кадр упавшей функции
исключение нигде не сохранено: payload после выхода из функции — мёртв
сохранено в локальную err: payload после выхода — жив
цикл: кадр -> err -> исключение -> __traceback__ -> кадр
после gc.collect(): мёртв
сохранено в svc.last_error: payload после выхода — жив
путь от живого svc: svc -> исключение -> __traceback__ -> кадр -> payload
gc.collect() при живом svc: жив
svc.last_error = None: мёртв
имя e после блока except: NameError — его удалил сам интерпретатор
Второй и третий случаи выглядят одинаково — «payload жив», — а устроены
по-разному, и от этого зависит лечение. Локальная err = e возвращает тот
самый цикл из справочника: после выхода из функции до него не добраться, и
объект ждёт сборщика. С атрибутом svc.last_error всё достижимо от живого
сервиса — и сборщик это не тронет: gc.collect() при живом svc объект не
освободил. Но и ждать его не нужно: svc.last_error = None, и объект умер на
этой же строке. Хранить в атрибуте стоит текст ошибки, а не
само исключение.
lru_cache на методе держит self
4) functools.lru_cache на методе
del rep: жив — self лежит в ключе кеша
gc.collect(): жив
Report.total.cache_clear(): мёртв
Кеш живёт на функции, то есть на классе, а self — часть ключа. Документация
Python описывает это в FAQ:
The disadvantage is that instances are kept alive until they age out of the cache or until the cache is cleared
(Недостаток в том, что экземпляры остаются живыми, пока их не вытеснят из кеша или пока кеш не очистят).
При maxsize=None вытеснения нет вовсе, и каждый экземпляр, у которого хоть
раз вызвали метод, живёт до cache_clear() или до конца процесса.
Метод в списке подписчиков держит объект
5) связанный метод в списке подписчиков
callbacks.append(w.on_event); del w: жив — метод держит __self__
gc.collect(): жив
callbacks.clear(): мёртв
то же через weakref.WeakMethod; del w: мёртв
weak_callbacks[0]() -> None
w.on_event — связанный метод, объект, у которого в __self__ лежит w.
Подписать метод на событие значит держать сильную ссылку на весь объект.
weakref.WeakMethod ссылается на него слабо: объект умирает, подписка
возвращает None, и её можно выбросить при следующей рассылке.
Механизм 3: __del__ в цикле с 3.4 не мешает сборке
До Python 3.4 цикл, в котором у объекта был __del__, сборщик не трогал:
было неясно, в каком порядке вызывать финализаторы, если объекты ссылаются друг на
друга. Такие объекты копились в gc.garbage. PEP 442 это снял:
to be able to define and run finalizers for any object, regardless of their position in the object graph
(чтобы можно было определять и запускать финализаторы для любого объекта, независимо от его положения в графе объектов).
6) __del__ у обеих вершин цикла
gc.collect() вернул 2; финализаторы выполнились: ['a', 'b']
gc.garbage: []
Оба финализатора вызваны, gc.garbage пуст. Но порядок, в котором они
вызываются, PEP явно оставляет неопределённым:
For CI objects, the order in which finalizers are called (step 2 above) is undefined
(Для объектов в циклической изоляции порядок вызова финализаторов (шаг 2 выше) не определён).
Здесь вышло a, потом b — полагаться на это нельзя. И в __del__ объекта
из цикла сосед может быть уже финализирован.
Глубже: за что платит полная сборка
Полная сборка — gc.collect() без аргументов — обходит все отслеживаемые
объекты, в том числе живые: чтобы понять, что группа недостижима, надо знать,
на что ссылаются остальные. Поэтому её время растёт с числом живых
контейнеров, даже когда мусора нет ни одного:
PY 3.13.7 | лучшее из 7 вызовов gc.collect()
живых списков gc.collect() на объект
10 000 0.75 мс 45.8 нс
100 000 3.57 мс 32.8 нс
1 000 000 39.88 мс 39.6 нс
пустая куча 0.29 мс
Несколько десятков наносекунд на живой объект — в этой таблице от 32,8 до 45,8 нс: миллион живых списков — почти 40 мс паузы на полную сборку. Сам сборщик запускает полную сборку редко — пороги рассчитаны на то, чтобы большая часть работы шла в младшем поколении, — но процесс с большим долгоживущим кешем в памяти платит за него на каждой такой сборке.
Убрать живое из обхода умеет gc.freeze(), появившийся в 3.7: он переносит
всё отслеживаемое в постоянное поколение, и следующие сборки его не обходят.
Но придуман он не как общее средство от пауз, а под один случай — о нём ниже.
тот же миллион живых списков
до gc.freeze(): 38545.7 мкс
после gc.freeze(): 0.3 мкс, в постоянном поколении 1 005 045 объектов
Документация предлагает его для процессов, которые делают fork без exec:
сборщик в дочернем процессе не трогает замороженные объекты, и меньше страниц
памяти копируется при записи. Как общий приём против пауз он годится с двумя
оговорками. Мусорные циклы среди замороженного больше не собираются, пока не
вызван gc.unfreeze(), — то есть это память, отданная навсегда. И в сборке
без GIL заморозка снимает со сборки не всё; это видно в следующем разделе.
Глубже: сборка без GIL — то же правило, но с исключениями
С 3.13 CPython собирается и без GIL (PEP 703). Счётчик ссылок там устроен иначе: у объекта есть поток-владелец, и операции из этого потока идут по быстрому пути, а из чужого — по общему. Как это сказывается на скорости, разобрано в статье о GIL, как на размере объекта — в статье об управлении памятью. Здесь — что это значит для вопроса «когда объект умрёт».
Главное правило урока держится: экземпляр, созданный и отпущенный одним
потоком, умирает на строке del и без GIL. Все шесть опытов bench/gc/alive.py на
3.13t и 3.14t дают тот же ответ «жив или мёртв», что с GIL. Но три вещи
расходятся:
2) функция, объявленная на уровне модуля
сразу после del: жив
после gc.collect(): мёртв
для сравнения, вложенная функция сразу после del: мёртв
3) последнюю ссылку удаляет не тот поток, что создал объект
жив в том потоке сразу после del: 19 из 20
жив в главном потоке после join(): 0 из 20
4) gc.freeze() и миллион живых списков, лучшее из 7 вызовов gc.collect()
до gc.freeze(): 37272.2 мкс
после gc.freeze(): 23155.0 мкс, в постоянном поколении 1 007 024 объектов
Функция уровня модуля ждёт сборщика. С GIL она умирает на строке del, без
GIL — только на gc.collect(); вложенная функция умирает сразу в обеих
сборках. Тот же эффект виден и в bench/gc/alive.py: в шестом опыте gc.collect() без
GIL вернул 3, а не 2, и третьим оказалась функция тела класса — с GIL её
освободил счётчик сразу после создания класса.
Последняя ссылка из чужого потока. Объект создал главный поток, а последнюю
ссылку забрал и удалил другой. Сразу после del в том потоке объект был жив в
19 попытках из 20 на 3.14t и в 20 из 20 на 3.13t; в главном после join() —
мёртв во всех. С GIL — мёртв сразу, 0 из 20 на всех четырёх версиях. Число
попыток напечатано не случайно: результат зависит от расписания потоков.
gc.freeze() снимает не всё. С GIL после заморозки сборка миллиона живых
списков занимает доли микросекунды — 0,2–0,3 мкс в этом же прогоне. Без GIL —
23,2 мс из 37,3 на 3.14t и 23,4 из 63,5 на 3.13t: заметная часть работы
сборщика остаётся.
Отсюда точная формулировка для собеседования: в обычной сборке CPython объект без цикла умирает сразу, когда счётчик доходит до нуля; в сборке без GIL это верно для объекта одного потока, но не для всех объектов.
Как отвечать на собеседовании
Короткий ответ: в CPython память освобождается прежде всего подсчётом
ссылок: объект умирает, как только на него пропадает последняя ссылка, сразу.
del удаляет имя, а не объект. Сборщик мусора дополняет счётчик и нужен для
циклов: объекты, ссылающиеся друг на друга, счётчик не освободит никогда, и
gc находит такие группы без внешних ссылок.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Если спросят про утечки, хороший ответ начинается не с циклов, а с того, что
объект может держать обычная ссылка от живого объекта, которой не видно:
исключение в атрибуте держит трассировку, кадр и все его локальные
переменные; lru_cache на методе держит self в ключе; метод в списке
подписчиков держит объект в __self__. Всё это достижимо, и сборщик такое не
тронет. Лечится одной правкой: хранить текст ошибки, чистить кеш,
подписываться через weakref.WeakMethod.
Стоит добавить, почему имя из except ... as e исчезает после блока: иначе
кадр держал бы исключение, исключение через трассировку держало бы кадр, и
получился бы цикл со всеми локальными переменными. Это написано в справочнике
языка. И про цену сборки: она растёт с числом живых объектов — миллион
живых списков, 39,88 мс на gc.collect() без мусора (3.13.7), — а
gc.freeze() убирает их из обхода — в обычной сборке.
Если речь зайдёт о сборке без GIL, стоит сказать, что «умирает на той же
строке» там верно не всегда: функция уровня модуля ждёт сборщика, а объект,
последнюю ссылку на который отпустил чужой поток, переживает del в этом
потоке.
Дальше спросят
Можно ли выключить сборщик?
Можно — gc.disable(), и документация это допускает, если программа не
создаёт циклов. Подсчёт ссылок продолжит работать, и объекты без циклов
будут освобождаться как раньше. Но любой цикл — включая цикл «кадр —
исключение» — будет жить до явного gc.collect() или до конца процесса. Мягче gc.freeze(): он убирает
из обхода то, что уже создано, а новые циклы продолжают собираться.
Как найти, кто держит объект?
gc.get_referrers(obj) возвращает объекты, которые ссылаются на него
напрямую, — документация просит использовать его только для отладки. По
цепочке таких вызовов доходят до держателя: атрибута, кеша, списка
подписчиков. Проверить, умер ли объект после правки, — слабой ссылкой, как в
прогонах урока.
А __del__ в цикле — это утечка?
С 3.4 — нет: PEP 442 разрешил сборщику финализировать такие циклы, и
gc.garbage пуст. Но порядок вызова финализаторов в цикле не определён, и в
__del__ сосед по циклу может быть уже финализирован.
Частые заблуждения
del x удаляет объект
Он удаляет имя x и уменьшает счётчик ссылок на единицу. Объект умирает, только если это была последняя ссылка. В цикле из двух вершин после del a, b обе живы и умирают только на gc.collect().
Память в Python освобождает сборщик мусора
Прежде всего — счётчик ссылок, и сразу: объект без цикла освобождается на той же строке, где пропала последняя ссылка, даже при выключенном сборщике. Сборщик дополняет счётчик и нужен только для циклов.
Если объект не освобождается, где-то цикл
Держать может и обычная ссылка от живого объекта, которой не видно в коде: исключение в атрибуте держит кадр и все его локальные переменные, lru_cache на методе держит self, связанный метод в списке подписчиков держит объект. Всё это достижимо: gc.collect() в прогоне такой объект не освободил, а одна правка освобождает его на той же строке.
Объекты с __del__ в цикле никогда не собираются
Так было до 3.4. С PEP 442 такие циклы собираются: в прогоне gc.collect() вернул 2, оба финализатора выполнились, gc.garbage пуст. Не определён только порядок вызова финализаторов.
Объект без ссылок умирает сразу в любой сборке CPython
Так в обычной сборке, с GIL. В сборке без GIL функция уровня модуля после del жива и умирает только на gc.collect(), а объект, последнюю ссылку на который удалил не тот поток, что его создал, в этом потоке сразу после del жив — 19 попыток из 20 на 3.14t. Экземпляр одного потока умирает сразу в обеих сборках.
Сборка мусора стоит тем дороже, чем больше мусора
Полная сборка обходит все отслеживаемые объекты, живые тоже. Без единого мусорного объекта: 0,75 мс при десяти тысячах живых списков и 39,88 мс при миллионе (3.13.7). После gc.freeze() тот же миллион в обход не попадает.
История версий
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| 3.4 | PEP 442: циклы с __del__ собираются, в gc.garbage они больше не попадают. До этого совет был — не допускать объектов с __del__ в циклах. | |
| 3.7 | Появляются gc.freeze(), gc.unfreeze() и gc.get_freeze_count() — способ убрать долгоживущие объекты из обхода сборщика, прежде всего перед fork. | |
| 3.13 | Порог младшего поколения по умолчанию — 2000 вместо 700. Проверено прогоном: gc.get_threshold() даёт (700, 10, 10) на 3.11.15 и 3.12.3 и (2000, 10, 10) на 3.13.7 и 3.14.7. Смена порога видна и в исходнике на теге v3.13.0; что именно считают эти числа — в статье об управлении памятью. И появляется сборка без GIL (PEP 703, в sys.version — experimental free-threading build): экземпляр одного потока умирает на строке del и там, а функция уровня модуля ждёт сборщика (bench/gc/freethreaded.py). | |
| 3.14 | В обычной сборке: в 3.14.0 сборщик стал инкрементальным, в 3.14.5 это откатили — подробности и источник в статье об управлении памятью. На поведение, разобранное в этом уроке, это не влияет: все шесть опытов bench/gc/alive.py дают на 3.14.7 тот же вывод, что на 3.11–3.13. Сборка без GIL (в sys.version — уже free-threading build, без experimental) собрана с другим сборщиком — путь исходника в библиотеке Python/gc_free_threading.c против Python/gc.c — и ведёт себя иначе: gc.freeze() снижает полную сборку миллиона живых списков с 37,3 до 23,2 мс, а не до долей микросекунды. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
class Node: pass gc.disable() a = Node() b = Node() a.other = b b.other = a r = weakref.ref(a) del a, b print(r() is None) gc.collect() print(r() is None)
Практика · оцените
Проверьте себя
Объект без циклов, на него одна ссылка — имя x. Сборщик выключен через gc.disable(). Что происходит на del x?
Чем измерено
Числа этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- В обычной сборке CPython объект умирает, когда пропадает последняя ссылка на него, — сразу, на этой же строке. Этим занимается счётчик ссылок, а не сборщик мусора.
delне уничтожает объект, а убирает имя и уменьшает счётчик на единицу. Сборщик (gc) нужен ровно для одного случая, который счётчик не умеет: цикл. Две вершины ссылаются друг на друга, имён больше нет, а счётчики остаются по единице — и объекты живут доgc.collect(), который находит группу без внешних ссылок и освобождает её. - Дальше — то, что отличает знающего от читавшего. Когда память «не освобождается», искать стоит не только мусорный цикл, но и ссылку от живого объекта, которой не видно в коде. Исключение, сохранённое в атрибут, держит трассировку, трассировка — кадр упавшей функции, кадр — все его локальные переменные.
lru_cacheна методе держитselfв ключе. Метод, положенный в список подписчиков, держит свой объект в__self__. Всё это достижимо от живого объекта, и сборщик этого не тронет —gc.collect()в прогоне ничего не менял. Зато одна правка рвёт путь, и объект умирает на той же строке. А за полную сборку платят не мусором, а живыми объектами: миллион живых списков — 39,88 мс наgc.collect()без единого мусорного объекта (3.13.7). - С 3.13 у CPython две сборки, и всё сказанное выше — про обычную, с GIL. В сборке без GIL (3.13t, 3.14t) экземпляр, созданный и отпущенный одним потоком, тоже умирает на строке
del. Но функция уровня модуля послеdelждёт сборщика, объект, последнюю ссылку на который отпустил чужой поток, эту строку переживает, аgc.freeze()снимает со сборки не всё.
На самом деле
- Он удаляет имя
xи уменьшает счётчик ссылок на единицу. Объект умирает, только если это была последняя ссылка. В цикле из двух вершин послеdel a, bобе живы и умирают только наgc.collect(). - Прежде всего — счётчик ссылок, и сразу: объект без цикла освобождается на той же строке, где пропала последняя ссылка, даже при выключенном сборщике. Сборщик дополняет счётчик и нужен только для циклов.
- Держать может и обычная ссылка от живого объекта, которой не видно в коде: исключение в атрибуте держит кадр и все его локальные переменные,
lru_cacheна методе держитself, связанный метод в списке подписчиков держит объект. Всё это достижимо:gc.collect()в прогоне такой объект не освободил, а одна правка освобождает его на той же строке. - Так было до 3.4. С PEP 442 такие циклы собираются: в прогоне
gc.collect()вернул 2, оба финализатора выполнились,gc.garbageпуст. Не определён только порядок вызова финализаторов. - Так в обычной сборке, с GIL. В сборке без GIL функция уровня модуля после
delжива и умирает только наgc.collect(), а объект, последнюю ссылку на который удалил не тот поток, что его создал, в этом потоке сразу послеdelжив — 19 попыток из 20 на 3.14t. Экземпляр одного потока умирает сразу в обеих сборках. - Полная сборка обходит все отслеживаемые объекты, живые тоже. Без единого мусорного объекта: 0,75 мс при десяти тысячах живых списков и 39,88 мс при миллионе (3.13.7). После
gc.freeze()тот же миллион в обход не попадает.
По версиям
- 3.4
- PEP 442: циклы с
__del__собираются, вgc.garbageони больше не попадают. До этого совет был — не допускать объектов с__del__в циклах.< - 3.7
- Появляются
gc.freeze(),gc.unfreeze()иgc.get_freeze_count()— способ убрать долгоживущие объекты из обхода сборщика, прежде всего передfork.< - 3.13
- Порог младшего поколения по умолчанию — 2000 вместо 700. Проверено прогоном:
gc.get_threshold()даёт(700, 10, 10)на 3.11.15 и 3.12.3 и(2000, 10, 10)на 3.13.7 и 3.14.7. Смена порога видна и в исходнике на теге v3.13.0; что именно считают эти числа — в статье об управлении памятью. И появляется сборка без GIL (PEP 703, вsys.version—experimental free-threading build): экземпляр одного потока умирает на строкеdelи там, а функция уровня модуля ждёт сборщика (bench/gc/freethreaded.py).< - 3.14
- В обычной сборке: в 3.14.0 сборщик стал инкрементальным, в 3.14.5 это откатили — подробности и источник в статье об управлении памятью. На поведение, разобранное в этом уроке, это не влияет: все шесть опытов
bench/gc/alive.pyдают на 3.14.7 тот же вывод, что на 3.11–3.13. Сборка без GIL (вsys.version— ужеfree-threading build, безexperimental) собрана с другим сборщиком — путь исходника в библиотекеPython/gc_free_threading.cпротивPython/gc.c— и ведёт себя иначе:gc.freeze()снижает полную сборку миллиона живых списков с 37,3 до 23,2 мс, а не до долей микросекунды.<
Что разобрано
- База: объект умирает, когда пропадает последняя ссылка
- Механизм 1: цикл — единственное, чего счётчик не умеет
- Механизм 2: объект жив не из-за цикла, а из-за ссылки, которой не видно
- Механизм 3: `__del__` в цикле с 3.4 не мешает сборке
- Глубже: за что платит полная сборка
- Глубже: сборка без GIL — то же правило, но с исключениями
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Справочник языка — оператор try, ветка exceptОфициальная документация. Самое прямое объяснение того, как трассировка держит память: «Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs» (Исключения удаляются, потому что вместе с прикреплённой трассировкой они образуют цикл ссылок с кадром стека, и все локальные переменные этого кадра остаются живыми до следующей сборки мусора). Отсюда и то, что имя после except as исчезает.https://docs.python.org/3.14/reference/compound_stmts.html#except-clause
- gc — интерфейс сборщика мусораОфициальная документация. Роль сборщика: «the collector supplements the reference counting already used in Python» (сборщик дополняет подсчёт ссылок, который в Python уже есть). gc.collect: «With no arguments, run a full collection» (Без аргументов выполняет полную сборку). gc.freeze, добавлен в 3.7: «Freeze all the objects tracked by the garbage collector; move them to a permanent generation and ignore them in all the future collections» (Заморозить все объекты, отслеживаемые сборщиком: перенести их в постоянное поколение и игнорировать во всех будущих сборках).https://docs.python.org/3.14/library/gc.html
- PEP 442 — Safe object finalizationPEP. Антуан Питру, Final, Python 3.4. До него объект с __del__ в цикле сборщик не трогал. Цель PEP: «to be able to define and run finalizers for any object, regardless of their position in the object graph» (чтобы можно было определять и запускать финализаторы для любого объекта, независимо от его положения в графе объектов). И граница гарантии: «For CI objects, the order in which finalizers are called (step 2 above) is undefined» (Для объектов в циклической изоляции порядок вызова финализаторов (шаг 2 выше) не определён).https://peps.python.org/pep-0442/
- Programming FAQ — How do I cache method calls?Официальная документация. Почему lru_cache на методе держит экземпляры: «It creates a reference to the instance unless special efforts are made to pass in weak references» (Он создаёт ссылку на экземпляр, если специально не передавать слабые ссылки) и «The disadvantage is that instances are kept alive until they age out of the cache or until the cache is cleared» (Недостаток в том, что экземпляры остаются живыми, пока их не вытеснят из кеша или пока кеш не очистят).https://docs.python.org/3/faq/programming.html#faq-cache-method-calls
- PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Сэм Гросс, Final, Python 3.13. Сборка CPython без GIL и её счётчик ссылок: у объекта есть поток-владелец, и операции из этого потока идут по быстрому пути, из чужого — по общему; основание — наблюдение, что «most objects are only accessed by a single thread, even in multi-threaded programs» (к большинству объектов обращается только один поток — даже в многопоточных программах). Что из этого следует для времени жизни объекта, урок проверяет прогоном
bench/gc/freethreaded.pyна 3.13t и 3.14t, а не пересказом.https://peps.python.org/pep-0703/ - weakref — слабые ссылкиОфициальная документация. Определение, на котором держится проверка живости в прогонах урока: «A weak reference to an object is not enough to keep the object alive» (Слабой ссылки на объект недостаточно, чтобы удержать его в живых). И WeakMethod: «Since a bound method is ephemeral, a standard weak reference cannot keep hold of it» (Поскольку связанный метод эфемерен, обычная слабая ссылка не может его удержать).https://docs.python.org/3/library/weakref.html