Сборка мусора в Python: поколения, пороги и ручная настройка
В CPython два механизма очистки памяти: подсчёт ссылок и поколенческий сборщик для циклов. Разбираем, когда именно запускается gc, как читать его пороги и в каких случаях его стоит трогать руками.

Долгоживущий процесс на CPython 3.12 держит в памяти сотни тысяч объектов и раз в несколько секунд замирает на десятки миллисекунд. Профилировщик показывает всплески в gc.collect. Это не утечка — это работа поколенческого сборщика, который на каждом проходе обходит все объекты старших поколений. Разберём, как он устроен, по какому условию запускается и что происходит, когда его пороги трогают руками.
Разбор для CPython 3.x, с оговорками по различиям между версиями. Речь только про механизм gc и подсчёт ссылок — арену аллокатора pymalloc и фрагментацию памяти не трогаем, это отдельная тема.
Два независимых механизма
В CPython память освобождают два разных механизма, и путать их — источник половины заблуждений про «медленный gc».
Подсчёт ссылок (reference counting) работает всегда и не отключается. У каждого объекта есть счётчик ob_refcnt. Когда он падает до нуля — объект уничтожается немедленно, прямо в точке, где ушла последняя ссылка. Никакой отложенности, никаких пауз на весь процесс. Основной объём памяти освобождается именно так.
Поколенческий сборщик (модуль gc) нужен ровно для одного: разорвать циклические ссылки. Два объекта, ссылающиеся друг на друга, никогда не обнулят счётчики сами по себе, даже когда снаружи на них никто не смотрит. Вот их и собирает gc. Именно этот механизм даёт паузы и именно его можно настраивать.
Отключить gc.disable() — не значит «отключить сборку мусора». Подсчёт ссылок продолжит работать. Вы отключаете только сбор циклов. Всё остальное освобождается как обычно.Что такое цикл на практике
Классический пример — двусвязный список, узел с полем parent, замыкание, держащее объект, который держит замыкание. Даже обычный объект, положивший self в свой же атрибут, образует цикл длиной один.
# CPython 3.12
import gc
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # цикл: a -> b -> a
del a, b # внешних ссылок нет, но счётчики не обнулятся
unreachable = gc.collect() # вернёт число собранных объектов
print(unreachable)Без вызова gc.collect() (или без автоматического прохода) эти два объекта провисели бы в памяти до завершения процесса.
Поколения и по какому условию запускается сборщик
Поколений три: 0, 1 и 2. Новый объект-контейнер попадает в поколение 0. Пережил сборку — переходит в старшее. Идея в том, что большинство объектов умирает молодыми, поэтому молодое поколение сканируют часто и дёшево, а старое — редко.
Сборщик запускается не по таймеру и не по объёму памяти в байтах. Он считает разницу между числом выделенных и освобождённых объектов-контейнеров. Когда счётчик поколения 0 превышает свой порог — запускается проход по нулевому поколению.
Пороги видны так:
# CPython 3.12
import gc
print(gc.get_threshold()) # например (700, 10, 10)- Первое число (700 по умолчанию) — порог поколения 0. Когда (число аллокаций − число деаллокаций) превысит его, идёт сборка поколения 0.
- Второе число (10) — сколько сборок поколения 0 должно пройти, прежде чем заодно соберут поколение 1.
- Третье число (10) — сколько сборок среднего уровня пройдёт до полной сборки, затрагивающей поколение 2.
Второе и третье числа — это множители частоты, а не пороги в объектах. Полная сборка поколения 2 — самая дорогая: она обходит все долгоживущие объекты. Именно она даёт заметные паузы в больших процессах.
Заметное изменение в новых версиях
Внутренняя реализация сборщика менялась между версиями. В частности, в CPython 3.12 переработали хранение флагов gc-состояния объектов. Точное поведение и наличие вспомогательных функций отладки уточняйте в документации модуля gc для вашей версии интерпретатора — API самого модуля стабильно, но детали внутренней механики между 3.9, 3.11 и 3.12 отличаются.
Замер: сколько это стоит
Абстрактные «десятки миллисекунд» бесполезны без условий. Померьте на своей нагрузке — стандартной библиотеки достаточно.
# CPython 3.12
import gc
import time
gc.collect() # чистый старт
# создаём много контейнеров с циклами
batch = []
for _ in range(500_000):
obj = {}
obj['self'] = obj # цикл
batch.append(obj)
start = time.perf_counter()
collected = gc.collect() # принудительная полная сборка
elapsed = time.perf_counter() - start
print(f'собрано объектов: {collected}')
print(f'полная сборка заняла: {elapsed * 1000:.1f} мс')Что здесь важно замерять помимо времени: сколько живых объектов остаётся в поколении 2 после сборки. Чем их больше, тем дороже каждая полная сборка, потому что все они обходятся заново. Число объектов по поколениям даёт gc.get_stats() — она возвращает список словарей со счётчиками сборок и собранных объектов на поколение.
Когда стоит трогать пороги руками
В большинстве приложений настройки по умолчанию адекватны, и первое правило — не трогать gc, пока профиль не показал, что паузы сборщика реально влияют на задержки. Но есть узнаваемые сценарии.
| Ситуация | Что сделать | Чем платите |
|---|---|---|
| Пачка запросов с жёстким требованием к хвостовым задержкам (p99), паузы gc портят метрику | Отключить сборщик на время обработки, собрать вручную между запросами | Риск роста памяти, если внутри запроса рождаются циклы |
| Долгий батч: загрузили большой граф объектов, дальше он живёт до конца процесса | После загрузки — gc.freeze(), чтобы вынести объекты из-под сканирования | Заморожённые объекты не собираются до размораживания |
| Форк-модель (например, prefork-воркеры): при копировании страниц gc помечает объекты и провоцирует copy-on-write | Перед форком gc.freeze(), отключить сборку в родителе | Нужно понимать жизненный цикл воркеров |
| Скрипт живёт секунды и завершается | gc.disable() целиком — циклы всё равно освободит выход из процесса | При долгой работе — утечка по циклам |
Приём с ручной сборкой между запросами
# CPython 3.12
import gc
gc.disable() # отключаем автоматические проходы
def handle_request(request):
# ... обработка, где не создаём длинных циклов ...
return build_response(request)
def after_batch():
# собираем вручную в паузе, когда задержка не критична
gc.collect()Смысл в том, чтобы паузы сборки происходили в предсказуемый момент, а не посреди горячего запроса. Но если внутри обработки рождаются циклы, а вы отключили gc, память будет расти до следующего ручного collect(). Следите за RSS.
Приём с gc.freeze()
Функция gc.freeze() перемещает все отслеживаемые объекты в постоянное хранилище, которое сборщик не сканирует. Полезно после загрузки большого неизменного состояния: интерпретатор перестанет обходить его на каждой полной сборке. Разморозить можно через gc.unfreeze(). Эта функция доступна начиная с Python 3.7.
Что ломается при ручной настройке
- Отключили gc и забыли собрать. Классика утечки: процесс медленно растёт по памяти, при этом никаких явных ссылок нет. Причина — циклы, которые больше некому разрывать. Диагностика: включить
gc.enable(), вызватьgc.collect(), посмотреть, упал ли RSS. - Слишком высокий порог поколения 0. Кажется, что редкие сборки — это хорошо. Но между сборками накапливается больше мусорных контейнеров, и когда сборка наконец идёт, она дороже и дольше. Пауза не исчезает, а становится реже и заметнее.
- Объекты с
__del__в цикле. В старых версиях (до 3.4) циклы с финализаторами не собирались и висели вgc.garbage. С 3.4 сборщик умеет их разрывать, но порядок вызова__del__в цикле не гарантирован — не закладывайтесь на него. - freeze без понимания жизненного цикла. Заморозили объекты, которые на самом деле должны освобождаться, — получили рост памяти, который выглядит как утечка, хотя это ваше собственное решение.
Как понять, что виноват именно gc
Прежде чем крутить пороги, подтвердите гипотезу. gc.set_debug(gc.DEBUG_STATS) печатает статистику каждой сборки в stderr: сколько объектов обошли, сколько собрали, сколько времени. Если в логе видно, что полные сборки идут часто и обходят сотни тысяч объектов — тогда есть смысл трогать пороги или замораживать состояние.
Что делать дальше
Начните с измерения, а не с настройки. Снимите gc.get_stats() на реальной нагрузке и посмотрите, сколько объектов живёт в поколении 2 и как часто идут полные сборки. Если полные сборки редки и дёшевы — не трогайте ничего. Если они дорогие и попадают в горячий путь — выбирайте приём под свой сценарий: ручная сборка в паузах для сервиса с жёстким p99, gc.freeze() для большого неизменного состояния, полное отключение для короткоживущих скриптов. После любого изменения замеряйте RSS в проде: главный риск ручной настройки — превратить редкую паузу в тихую утечку по циклам.
Разрушитель авто-разводов: Фото заказ-наряда СТО: завышения, дубли работ, лишние позиции.
Спросить за 15 ₽Источники: Python documentation — gc: Garbage Collector interface, CPython source — Modules/gcmodule.c and InternalDocs/garbage_collector.md
Частые вопросы
Если я вызову gc.disable(), память вообще перестанет освобождаться?
У меня другая версия Python, приёмы сработают?
Я отключил gc, а память растёт. Это точно утечка?
Зачем вообще нужен сборщик, если есть подсчёт ссылок?
Стоит ли поднимать пороги, чтобы сборка шла реже?
Что делать с объектами, у которых определён __del__, в циклах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.