Управление памятью в CPython: три цифры, которые все повторяют неправильно
Пороги сборщика мусора, размер арены и стоимость объекта — всё это давно не то, что написано в статьях. Каждое число здесь снято с работающего интерпретатора и сверено с исходником на закреплённом теге.
Полное техническое изложение
TL;DR
- Python убирает мусор двумя способами. Первый работает всё время и мгновенно, второй просыпается изредка и нужен только для одного особого случая.
- Первый способ — счёт: у каждого объекта записано, сколько на него ссылаются. Дошло до нуля — объект стирается сразу же.
- Второй нужен, когда объекты держат друг друга: счёт у них не нулевой, а пользы никакой. Вот их и ищет отдельный уборщик.
- Числа, которые про всё это обычно пишут, устарели. Проверять их — дело двух строк, и в статье показано каких.
Библиотека с записью на обложке
Представьте библиотеку, где на обложке каждой книги написано, сколько человек её сейчас держат.
Взяли книгу — прибавили к числу единицу. Вернули — отняли. Как только на обложке появился ноль, книгу немедленно увозят в макулатуру. Не вечером, не по расписанию — прямо в этот момент.
Так и работает Python. Объект — книга, число на обложке — счётчик ссылок.
a = SomeThing() # на обложке 1: держит имя a
b = a # на обложке 2: то же самое, но держат двое
del b # снова 1
del a # 0 — объект стёрт немедленноЗдесь важно одно слово: немедленно. Не «когда-нибудь потом», не «когда дойдут руки». В ту же миллисекунду.
Кстати, о числах на этой картинке. Если вы попробуете посмотреть счётчик сами, через sys.getrefcount, ответ будет на единицу больше ожидаемого. Причина забавная: чтобы спросить у объекта его счётчик, надо передать объект в функцию, а передача — это тоже ссылка. Измерительный прибор сам добавляет к тому, что измеряет.
Две книги с закладками друг на друга
Теперь случай, где счёт ломается.
Возьмите две книги и вложите в каждую закладку со ссылкой на другую. Читатели ушли, обе книги никому не нужны — но на обложках стоят единицы. Каждая книга держит другую.
По правилам библиотеки они живые. По факту их уже никогда никто не откроет.
Посмотрите на третий шаг: после того как убрали оба имени, счётчики не обнулились, а стали равны единице. Это и есть вся проблема. Счётчик не ошибается — он честно отвечает на вопрос «есть ли на объект хоть одна ссылка». Просто нужный вопрос другой: «можно ли до объекта вообще добраться из программы».
Для этого и существует второй механизм — уборщик, который время от времени обходит объекты и ищет такие замкнутые группы. В Python он называется сборщиком мусора, и — вопреки распространённому мнению — занимается только этим. Всё остальное убирает счётчик.
Как часто просыпается уборщик
Уборщик не работает постоянно, иначе программа бы только этим и занималась. Он ждёт, пока накопится достаточно новых объектов, и тогда делает проход.
Сколько именно — можно спросить:
import gc
gc.get_threshold()И вот тут начинается интересное. Почти в каждой статье написано, что ответ будет (700, 10, 10). Проверка на разных версиях:
| Версия | Ответ |
|---|---|
| 3.11 и 3.12 | (700, 10, 10) |
| 3.13 | (2000, 10, 10) |
| 3.14.0 (rc2) | (2000, 10, 0) |
| 3.14.7 | (2000, 10, 10) |
Число поменялось в версии 3.13 и с тех пор успело поменяться ещё дважды. А самое любопытное — в официальной документации эти значения не написаны вообще: страница про сборщик объясняет, что значат три порога, но ни одного числа не приводит. Проверено для 3.13 и 3.14.
Мораль простая и полезная: про память лучше не вспоминать, а спрашивать. Одна строка кода надёжнее любой статьи, включая эту.
Почему память не всегда возвращается
Ещё одна вещь, которая пугает при первой встрече.
Программа создала миллион мелких объектов, потом удалила их все. Счётчики обнулились, объекты стёрты — а память, которую занимает процесс, осталась прежней. Утечка?
Нет. Библиотека не заказывает полку под каждую книгу — она арендует комнаты целиком: Python берёт память у операционной системы большими кусками, на 64-битной сборке по мегабайту. Внутри такого куска он сам раздаёт места под объекты, и это быстро. Но вернуть кусок системе можно только целиком, когда он опустел полностью.
Достаточно, чтобы в мегабайтном куске остался жить один объект — и весь кусок останется за процессом. После пика нагрузки таких «почти пустых, но не совсем» кусков может набраться много.
Это не поломка и не утечка, это обычное поведение любого подобного распределителя памяти. И gc.collect() тут не поможет: он ищет замкнутые группы, а не уплотняет кучу.
Что стоит запомнить
Уборщик мусора в Python — не главный герой, а специалист по одному редкому случаю. Основную работу делает счёт ссылок, и делает мгновенно.
А ещё: почти любое число про память, которое вы прочитали, стоит перепроверить на своей версии. Не из недоверия к авторам — просто эти числа меняются тише, чем о них пишут.
TL;DR
- Память в CPython освобождают два механизма, а не один: счётчик ссылок делает это немедленно, циклический сборщик — потом и только для того, до чего счётчик не дотягивается.
- Пороги сборщика
(700, 10, 10), которые цитируют повсеместно, устарели в 3.13. Сейчас(2000, 10, 10), и по дороге они успели побывать(2000, 10, 0). - Арены по 256 КБ, пулы по 4 КБ и 64 класса размеров — описание 32-битной сборки. На 64-битной это 1 МиБ, 16 КиБ и 32 класса, потому что выравнивание там 16 байт, а не 8.
- В сборке без GIL заголовок объекта вырастает с 16 до 32 байт, а pymalloc заменён на mimalloc. Цена видна в
sys.getsizeof— но не у всех объектов, и причина этого сама по себе поучительна.
Зачем это знать?
Потому что почти всё, что написано про память в Python, описывает интерпретатор, которого у вас уже нет.
Это не преувеличение. Ниже — три числа, которые встречаются в каждой второй статье, и то, что на самом деле вернул интерпретатор при проверке. Все три разошлись с общепринятым, и разошлись недавно: два из них поменялись в 3.13, третье зависит от разрядности сборки уже давно, но продолжает жить в пересказах в старом виде.
Практическая польза не в самих числах, а в привычке. Память — та область, где «я читал, что» устаревает тише всего: gc.get_threshold() возвращает кортеж без предупреждений о том, что позавчера он был другим, а sys.getsizeof не сообщает, что в соседней сборке ответ отличается вдвое.
Модель в голове
Представьте библиотеку, где у каждой книги на обложке написано, сколько читателей её сейчас держат. Читатель взял — прибавил единицу, вернул — отнял. Как только счётчик обнулился, книга немедленно едет в макулатуру: не в конце дня, не по расписанию, а прямо в этот момент.
Схема работает безупречно ровно до одного случая. Две книги, в каждой закладка со ссылкой на другую. Оба читателя ушли, но счётчики не нулевые: книги держат друг друга. По правилам библиотеки они живые, по факту их никто никогда не откроет. Вот для этой — и только для этой — ситуации и существует второй механизм, который раз в какое-то время обходит полки и ищет замкнутые группы, недостижимые снаружи.
Всё остальное в этой статье — детали двух этих механизмов и цена, которую за них платят.
Счётчик ссылок: главный, а не запасной
Начнём с того, что обычно проговаривают вскользь: основную работу делает не сборщик мусора. Объект освобождается счётчиком ссылок, немедленно, и сборщик о нём не узнаёт вовсе — если объект не участвует в цикле. Циклами сборщик и занимается, больше ничем.
Каждый объект начинается с поля ob_refcnt. Макросы Py_INCREF и Py_DECREF его увеличивают и уменьшают, и когда Py_DECREF доводит счётчик до нуля, освобождение происходит прямо там, в этом вызове.
Обратите внимание на нижнюю плашку рисунка. Она про то, что кажется мелочью, но объясняет половину недоумений вокруг getrefcount: сам замер добавляет ссылку. Аргумент, переданный в функцию, — это ссылка, поэтому sys.getrefcount(x) всегда на единицу больше «настоящего» значения. А если завести удобную обёртку def rc(obj): return sys.getrefcount(obj) - 1, помеха вырастет до двух: параметр obj — ещё одна ссылка. Измерено на 3.13.13: на месте выражение даёт 1, через обёртку — 2.
Вывод не «getrefcount врёт», а более общий: у этого инструмента есть цена наблюдения, и её надо вычитать сознательно.
Слабая ссылка — это ссылка, которая не считается
Иногда объект нужно помнить, но не удерживать: кэш, реестр наблюдателей, обратная ссылка с ребёнка на родителя. Для этого есть weakref — ссылка, которая не увеличивает ob_refcnt.
import sys, weakref
class T: pass
t = T()
sys.getrefcount(t) - 1 # 1
r = weakref.ref(t)
sys.getrefcount(t) - 1 # 1 — слабая ссылка не посчиталась
r() is t # True
del t
r() # None — объект освобождён, ссылка это знаетЧисла в комментариях — вывод этого кода на 3.13.13. Важна последняя строка: слабая ссылка не превращается в висячий указатель, она честно начинает возвращать None.
Цикл: единственное, чего счётчик не умеет
Теперь та самая пара книг с закладками друг на друга.
Числа на рисунке стоит прочитать внимательно, потому что тут обычно и возникает путаница. После del left; del right счётчики не нулевые и не «непонятные» — они равны единице. Каждый объект держит другой, и с точки зрения счётчика это ничем не отличается от живой структуры данных.
Счётчик ссылок здесь не ошибается. Он правильно отвечает на свой вопрос — «есть ли на объект хоть одна ссылка». Просто нужный вопрос другой: «достижим ли объект из работающей программы». Разница между этими двумя вопросами и есть вся причина, по которой в CPython существует второй сборщик.
В замере gc.collect() вернул 2 и вызвал оба __del__. До этого не вызвался ни один.
Пороги: число, которое все помнят неправильно
Сборщик циклов не работает постоянно. Он просыпается, когда разница между числом созданных и числом уничтоженных отслеживаемых объектов превысила порог.
Что именно считают эти три числа
Прежде чем разбираться, какие числа правильные, стоит понять, что они настраивают: без этого (700, 10, 10) выглядит как три однородных значения, а это не так.
Поколений три, и это три списка. Каждый отслеживаемый объект лежит ровно в одном из них. Только что созданный попадает в нулевое. Тот, кто пережил сборку своего поколения, переезжает в следующее — и попадает под сборку реже.
Смысл деления — не в аккуратности, а в стоимости. Полный обход кучи стоит времени, пропорционального её размеру, а поколенческая сборка исходит из того, что большая часть объектов умирает почти сразу после создания: временный список в цикле, распакованный кортеж, промежуточная строка. Обходить ради них всю кучу, включая структуры, живущие с запуска процесса, — платить за проверку того, что почти наверняка не изменилось. Поколения позволяют часто и дёшево смотреть туда, где смертность высокая, и редко — туда, где низкая.
Теперь главное, из-за чего три числа кажутся однородными. Они меряют разное:
| Порог | Что считает его счётчик | Когда счётчик растёт |
|---|---|---|
threshold0 | объекты | растёт при создании отслеживаемого объекта, уменьшается при уничтожении — с порогом сравнивают разницу |
threshold1 | сборки поколения 0 | каждый раз, когда собрали нулевое поколение |
threshold2 | сборки поколения 1 | каждый раз, когда собрали первое |
То есть (2000, 10, 10) читается так: собрать нулевое поколение после 2000 новых объектов; каждую десятую такую сборку заодно собрать первое; каждую десятую сборку первого — заодно второе. Первое число — про объекты, два других — про сборки. Отсюда и порядок величин: 2000 против 10 — это не «один порог намного больше», это разные единицы измерения.
Двусмысленность зашита в сам исходник — в структуре поколения одно поле count обслуживает оба случая, и комментарий к нему честно перечисляет оба:
struct gc_generation {
PyGC_Head head;
int threshold; /* collection threshold */
int count; /* count of allocations or collections of younger
generations */
};Механику выбора видно в одном цикле — это gc_select_generation() из Python/gc.c на теге v3.13.0:
for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
if (gcstate->generations[i].count > gcstate->generations[i].threshold) {
if (i == NUM_GENERATIONS - 1
&& gcstate->long_lived_pending < gcstate->long_lived_total / 4)
{
continue;
}
return i;
}
}Цикл идёт от старшего поколения к младшему и берёт первое, чей счётчик превысил порог. Собрав поколение i, сборщик обходит его вместе со всеми младшими, обнуляет их счётчики и добавляет единицу к счётчику следующего:
if (generation+1 < NUM_GENERATIONS) {
gcstate->generations[generation+1].count += 1;
}
for (i = 0; i <= generation; i++) {
gcstate->generations[i].count = 0;
}И тут же — условие, которого нет ни в документации, ни в пересказах. Строчка с long_lived_pending означает, что для самого старшего поколения превышения порога недостаточно. Полная сборка пропускается, если число объектов, переживших все неполные сборки и ещё ни разу не проверенных полной, меньше четверти от числа переживших последнюю полную. Проще: если с прошлого полного обхода долгоживущих почти не прибавилось, обходить всё заново незачем.
Практическое следствие важнее самого условия: threshold2 = 10 — необходимое условие полной сборки, но не достаточное. Программа, у которой куча долгоживущих объектов давно устоялась, может пройти сотни сборок первого поколения и не увидеть ни одной полной. Утверждение «полная сборка происходит каждые сто сборок нулевого поколения» арифметически стройное и неверное.
А теперь — сами числа
Порогов три, и почти в любой статье они приведены как (700, 10, 10). Проверка на пяти интерпретаторах:
| Сборка | gc.get_threshold() |
|---|---|
| 3.11.15 | (700, 10, 10) |
| 3.12.3 | (700, 10, 10) |
| 3.13.13 | (2000, 10, 10) |
| 3.14.0rc2 | (2000, 10, 0) |
| 3.14.7 | (2000, 10, 10) |
Это не особенность конкретных сборок. В Include/internal/pycore_runtime_init.h на теге v3.12.0 стоит { .threshold = 700, }; в том же файле на v3.13.0 — уже 2000.
Отдельно любопытно, что в документации gc значения по умолчанию не описаны вообще — проверено для 3.13 и 3.14: страница объясняет смысл трёх порогов, но не приводит ни одного числа. Откуда именно 700 разошлось по статьям, утверждать не берусь. Достоверно другое: подтверждения в документации у этого числа нет, зато у gc.get_threshold() оно есть всегда — и отвечает за текущую сборку.
Проверить действующий порог можно за десять строк:
import gc
gc.collect()
first = gc.get_threshold()[0]
gen1 = gc.get_count()[1]
keep = []
for i in range(1, 4001):
keep.append([i]) # список отслеживается сборщиком
if gc.get_count()[1] != gen1: # поколение 0 собрали
print(first, i)
breakНа 3.13.13 печатается 2000 2000, на 3.12.3 — 700 700, и так пять запусков из пяти: сборка срабатывает ровно на объекте с номером, равным порогу.
Одна оговорка: на 3.14.0rc2 этот же код за 4000 объектов не печатает ничего. Там третий порог равен нулю, сборка идёт приращениями, и счётчик поколений ведёт себя иначе — что само по себе неплохая иллюстрация к следующему разделу.
История с откатом, которую стоит знать целиком
Третье число в строке 3.14.0 — ноль, и это не опечатка.
В 3.13.0a6 в CPython появился инкрементальный сборщик циклов. Формулировка из журнала изменений: реализован инкрементальный сборщик, старое поколение собирается порциями, полный обход кучи больше не нужен, число поколений уменьшено с трёх до двух. Смысл — убрать длинные паузы на больших кучах.
В релиз 3.13 он не попал, а в 3.14.0 попал. Тогда же третий порог перестал что-либо значить: документация 3.14 помечает threshold2 как игнорируемый, а gc.collect(1) — как «выполнить приращение сборки» вместо «собрать поколение 1». Отсюда и (2000, 10, 0) в замере на 3.14.0rc2.
А затем — редкий случай — изменение откатили в исправляющем выпуске:
Python 3.14.0-3.14.4 shipped with a new incremental GC. However, due to a number of reports of significant memory pressure in production environments, it has been reverted back to the generational GC from 3.13.
Дословно: версии 3.14.0–3.14.4 вышли с новым инкрементальным сборщиком, но из-за ряда сообщений о существенном росте потребления памяти в production его вернули к поколенческому сборщику из 3.13. Запись в журнале изменений 3.14.5rc1 (gh-142516) уточняет, что откат сделан для сборки по умолчанию, а сборщик свободнопоточной сборки оставлен без изменений. Это не значит, что там остался инкрементальный: у сборки без GIL с 3.13.0a4 своя реализация сборщика, которая ищет объекты через mimalloc и, по формулировке того же журнала, не является поколенческой. Инкрементальный сборщик её не касался ни до, ни после отката.
Отсюда практический вывод для тех, кто держит большие кучи: строка «Python 3.14» в требованиях недостаточно точна. Поведение сборщика в 3.14.3 и 3.14.5 разное, и разница ровно в том, из-за чего откатывали.
Где лежат сами объекты: арены, пулы, блоки
Второй механизм — не сборка, а распределение. Просить у операционной системы память под каждый объект по отдельности слишком дорого: объекты в Python мелкие и создаются миллионами. Поэтому CPython берёт память крупными кусками и раздаёт её сам.
Уровня три: арена → пул → блок.
Класс размера: почему их 32 и зачем они вообще
Это место обычно проговаривают одной фразой — «объекты раскладываются по классам размеров», — и читатель остаётся с ощущением, что классы просто есть. Между тем именно они делают выделение дешёвым, и понять их стоит до всех чисел.
Класс размера — это множество запросов, которые получают блок одного и того же размера. Запрос округляется вверх до ближайшего кратного шестнадцати: и 1 байт, и 16 получают блок в 16 байт; и 17, и 32 — блок в 32; и так до 512. Отсюда 32 класса: 512 ÷ 16.
Шестнадцать — это ALIGNMENT 64-битной сборки, и число классов зависит напрямую от выравнивания: 512 ÷ 16. На 32-битной выравнивание равно восьми, и классов там 64. Дальше в статье это разобрано подробнее; здесь важно только, что «32» — не свойство pymalloc вообще, а свойство сборки, на которой сняты все числа этой статьи.
В исходнике вычисление класса — одна строка арифметики, номер получается сдвигом:
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;Ключевое — правило, ради которого всё построено: один пул обслуживает ровно один класс. Все блоки внутри пула одинакового размера, а значит взаимозаменяемы. И это превращает выделение памяти в две операции над односвязным списком:
- выделить — снять первый блок со списка свободных этого пула;
- освободить — положить блок обратно в начало списка.
Ни поиска подходящей дырки, ни разрезания большого куска на меньший, ни склейки соседних свободных участков — всего того, чем занят обычный malloc общего назначения. Он вынужден этим заниматься именно потому, что у него запросы любого размера вперемешку. pymalloc платит за отказ от этой работы округлением: часть блока пропадает впустую, зато выделение стоит несколько инструкций. Сколько именно пропадает, интерпретатор считает сам — цифра будет ниже, в разборе вывода sys._debugmallocstats().
Поэтому ответ на «почему классы разные» — чтобы внутри одного пула всё было одинаковым. Разнообразие вынесено на уровень выше: пулов много, и каждый занят своим размером.
Как память заполняется и как освобождается
Заполнение идёт снизу вверх и только по необходимости: следующий уровень включается, лишь когда предыдущего не хватило:
- Программа просит 100 байт. Округление вверх даёт класс 112 байт.
- Аллокатор ищет пул этого класса, в котором есть свободный блок, и снимает блок со списка свободных.
- Если такого пула нет — берётся пустой пул (из уже имеющейся арены) и назначается классу 112. Пул не рождается со своим размером, он его получает при первом использовании.
- Если пустых пулов не осталось — у операционной системы запрашивается новая арена в 1 МиБ, и она нарезается на 64 пула.
Освобождение идёт теми же тремя уровнями, но каждый следующий уровень требует более сильного условия:
| Что освобождается | Условие | Куда возвращается |
|---|---|---|
| блок | объект удалён | в список свободных своего пула — мгновенно, без участия ОС |
| пул | освободились все его блоки | в запас пустых пулов арены, может быть переназначен другому классу |
| арена | освободились все её пулы | операционной системе |
Вот эта асимметрия и есть причина репутации «Python не отдаёт память». Вниз память идёт мгновенно и по одному блоку, вверх — только целыми уровнями и только при полной пустоте. Один живой объект удерживает свой пул; один непустой пул удерживает арену в мегабайт.
Все три правила проще увидеть, чем прочитать: первая вкладка показывает, что происходит с запросом до выдачи памяти, вторая — переиспользование блока внутри пула, третья — почему процесс не отдаёт мегабайт из-за одного живого объекта.
Здесь и живёт вторая устаревшая цифра. Классическое описание — «арены по 256 КБ, пулы по 4 КБ, выравнивание 8 байт, 64 класса размеров» — верно для 32-битной сборки. На 64-битной ARENA_BITS равен 20, а POOL_BITS — 14, то есть 1 МиБ и 16 КиБ; выравнивание 16 байт, классов размеров 32. Интерпретатор говорит это сам:
Small block threshold = 512, in 32 size classes
2 arenas * 1048576 bytes/arena = 2,097,152
23 unused pools * 16384 bytes = 376,832
Это вывод sys._debugmallocstats() на голом старте 3.13.13. Читать его надо по-разному: первая строка и размеры арены с пулом — константы сборки, они одинаковы от запуска к запуску. А вот число арен и пулов — состояние конкретного процесса в конкретный момент; на трёх подряд запусках получилось 2 арены и 22, 23, 23 пула. Таблица занятости по классам размеров на вкладке «Блок» рисунка выше — такой же снимок одного процесса, а не справочные значения.
Что до «64 класса размеров» из классических описаний — цифра верная и относится именно к классам: на 32-битной сборке ALIGNMENT равен 8, и 512 ÷ 8 действительно даёт 64 класса. На 64-битной выравнивание 16, поэтому классов вдвое меньше. Совпадение с числом пулов в арене (1 МиБ ÷ 16 КиБ = 64) — просто совпадение, и путать эти две шестьдесятчетвёрки не стоит.
Две ловушки, если захотите повторить замер. Во-первых, sys._debugmallocstats() печатает в stderr, а не в stdout. Во-вторых — и это неочевидно — печатает он из C, поэтому contextlib.redirect_stderr его не перехватывает: подмена работает на уровне объекта sys.stderr, а не файлового дескриптора. Перехват «срабатывает», возвращает пустую строку, и разбор молча даёт пустой результат. Нужен os.dup2.
Что из этого следует на практике
Запрос до 512 байт включительно попадает в pymalloc и округляется вверх до ближайшего из 32 классов. Всё, что больше, идёт напрямую в системный malloc. Граница именно такая: в obmalloc.c условие записано как nbytes > SMALL_REQUEST_THRESHOLD, то есть ровно 512 байт ещё обслуживает pymalloc — в таблице классов это последний, 31-й.
Это объясняет две вещи, которые иначе выглядят странно. Округление до класса размера — та самая «квантизация»: в том же выводе на голом старте на неё ушло 7 552 байта. А арена возвращается операционной системе, только когда полностью опустела, — да и то не всегда: последнюю свободную арену аллокатор придерживает, чтобы не отдавать и тут же просить её обратно. Поэтому долгоживущий процесс, однажды создавший миллион объектов и удаливший их, может не вернуть память: достаточно, чтобы в каждой арене остался один живой объект.
Чем отличить удержанную арену от утечки
Предыдущий раздел объясняет, почему RSS процесса может не вернуться: арена отдаётся системе только целиком, а один живой объект держит пул, пул — арену. Отсюда практический вопрос, на который механизм ответа не даёт: я смотрю на растущий RSS — это утечка или это арены?
Инструмент для этого один — tracemalloc. Он считает не страницы процесса, а
байты, запрошенные у аллокатора объектов CPython, и именно это различие делает
его пригодным (bench/memory/tracemalloc_vs_rss.py).
Три случая, и по паре чисел они различаются однозначно.
Объекты живы — растут оба. На пике: RSS +47,55 МиБ, tracemalloc
+9,24 МиБ. Числа разные, и это нормально: в RSS входит и служебная таблица
самого tracemalloc, и накладные расходы аллокатора. Читать 47,55 как «объекты
весят столько» нельзя.
Объекты удалены, память не вернулась — расходятся (здесь и ниже вывод скрипта сокращён до относящихся к делу строк):
1b. 400 000 объектов созданы, затем удалены:
на пике (объекты живы) RSS +47.55 МиБ tracemalloc +9.24 МиБ
после del + gc.collect() RSS +31.53 МиБ tracemalloc +0.00 МиБ
tracemalloc вернулся к нулю — значит, ни одного живого объекта не осталось, и
искать утечку в коде не надо. Тридцать мегабайт держит аллокатор, ровно так, как
описано выше. Проверяется это следующим кругом:
2. Тот же цикл ещё три раза подряд, пик каждого:
круг 1 RSS +16.03 МиБ tracemalloc +9.24 МиБ
круг 2 RSS +5.00 МиБ tracemalloc +9.24 МиБ
круг 3 RSS +5.00 МиБ tracemalloc +9.24 МиБ
tracemalloc одинаков на всех кругах — работа та же. А RSS после первого круга
падает втрое и дальше не растёт вовсе: память берётся из уже удержанных арен.
Это и есть подпись «аллокатор держит», а не «течёт»: у утечки третий круг стоил
бы столько же, сколько первый. Снято на 3.13.7; на трёх подряд прогонах числа
повторились до сотых.
Растёт RSS, tracemalloc молчит вовсе.
1a. mmap на 64 МиБ, все страницы тронуты:
RSS +64.19 МиБ tracemalloc +0.00 МиБ
Так выглядит всё, что взято мимо аллокатора объектов: mmap, буфер C-библиотеки,
стек потока. tracemalloc перехватывает домены аллокатора CPython и про эту
память не знает по устройству, а не по недосмотру.
Диагностическая рамка целиком:
tracemalloc | RSS | вывод |
|---|---|---|
| растёт | растёт | утечка в объектах Python — смотрите snapshot.statistics('lineno') |
| вернулся | остался | аллокатор держит арены; следующий круг возьмёт из них |
| не растёт | растёт | память вне доменов аллокатора: mmap, C-библиотека, стеки потоков |
Первая строка — единственная, где виноват ваш код на Python, и там же
tracemalloc показывает виноватую строку с точностью до номера. Две другие
лечатся не поиском утечки.
Цена сборки без GIL, измеренная линейкой
Здесь живёт третья устаревшая цифра — стоимость самого объекта.
PEP 703 (Sam Gross, статус Final, Python 3.13) убирает GIL. Для памяти это означает три конкретных изменения, и все три видны замером.
Первое: заголовок объекта стал вдвое больше. В обычной сборке struct _object — это счётчик и указатель на тип. В сборке без GIL — совсем другая структура: ob_tid (идентификатор владеющего потока), ob_mutex (пообъектная блокировка), ob_gc_bits, ob_ref_local (локальный счётчик) и ob_ref_shared (разделяемый, атомарный). Это и есть biased reference counting: обычный поток трогает свой локальный счётчик без атомарных операций, а чужие потоки — разделяемый.
Цена видна невооружённым глазом:
| Объект | 3.14 с GIL | 3.14t без GIL |
|---|---|---|
object() | 16 Б | 32 Б |
1.5 | 24 Б | 40 Б |
пустой dict | 64 Б | 64 Б |
пустой list | 56 Б | 56 Б |
Шестнадцать байт добавляется каждому объекту, у которого нет ничего, кроме заголовка. А вот почему list и dict весят одинаково в обеих сборках — вопрос интереснее, и «оно потерялось в выравнивании» тут неверный ответ.
Дело в другом. Объект, за которым следит циклический сборщик, в обычной сборке несёт перед собой служебную структуру PyGC_Head — два указателя, 16 байт, и sys.getsizeof их учитывает. В сборке без GIL этой структуры нет вовсе: состояние сборщика переехало в поле ob_gc_bits внутри самого заголовка. Получается ровный размен: заголовок вырос на 16 байт, PyGC_Head на 16 байт исчез.
Арифметика сходится на пустом списке: 40 байт полей плюс 16 байт PyGC_Head в сборке с GIL — и 56 байт полей без всякого PyGC_Head в сборке без него. В обоих случаях 56. У object(), за которым сборщик не следит, компенсировать нечего — там рост виден целиком.
Второе: pymalloc заменён на mimalloc. PEP 703 объясняет причину прямо: реализация pymalloc не потокобезопасна без GIL. Замена видна в том же sys._debugmallocstats() — у двух сборок разный словарь:
с GIL: Small block threshold = 512, in 32 size classes
без GIL: Small block threshold = 16384, in 73 size classes
Medium block threshold = 131072
Large object max size = 16777216
Ни «арен», ни «пулов» во второй сборке нет вовсе. Это другой аллокатор, а не настроенный по-другому старый.
Третье: бессмертных объектов стало больше. Объекты, живущие всё время работы программы — None, True, малые целые, интернированные строки, — получают фиксированный счётчик ссылок и перестают его менять (PEP 683, Python 3.12). Значение константы менялось: в 3.12 и 3.13 getrefcount(None) возвращает 4294967295, в 3.14 — 3221225472, что ровно 3ULL << 30 из Include/refcount.h.
Но интереснее сравнение сборок на одной версии:
| Выражение | 3.14 с GIL | 3.14t без GIL |
|---|---|---|
getrefcount(None) | 3221225472 | 3221225472 |
getrefcount(257) | 3 | 3221225472 |
Число 257 — не малое целое, в обычной сборке это самый обыкновенный объект со счётчиком 3. В сборке без GIL записанный в коде литерал 257 оказывается бессмертным.
Формулировать это надо аккуратно, иначе получится неправда. Бессмертно не «число 257», а константа кодового объекта: то же самое число, полученное вычислением, ведёт себя обычно.
sys.getrefcount(257) # 3221225472 — литерал из кода
a = 256; b = a + 1
sys.getrefcount(b) # 2 — то же значение, обычный объект:
# одна ссылка b плюс аргументВывод снят на 3.14.0rc2t. Причина в том, что free-threaded сборка интернирует и делает бессмертными большинство констант кодовых объектов (Objects/codeobject.c, функция should_immortalize_constant) — так снимается состязание за их счётчик между потоками. Общая логика та же, что в PEP 703: бессмертие убирает атомарные операции над счётчиком. Но список объектов, которые PEP 703 объявляет бессмертными, — это набор из PEP 683 (интернированные строки, малые целые, статические типы, True/False/None), и константы кодовых объектов в него не входят: их добавила уже реализация.
Что здесь гарантия языка, а что — деталь реализации
Разделение принципиальное, потому что почти всё в этой статье — второе.
| Утверждение | Статус |
|---|---|
| Объект освобождается, когда на него не осталось ссылок | Деталь CPython. Спецификация языка не обещает когда |
| Циклы собирает отдельный сборщик | Деталь CPython |
Пороги (2000, 10, 10) | Деталь CPython, менялась в 3.13 и внутри 3.14 |
| Арена 1 МиБ, пул 16 КиБ | Деталь CPython, зависит от разрядности сборки |
| Заголовок объекта 16 или 32 байта | Деталь CPython, зависит от режима сборки |
weakref не удерживает объект | Задокументированное поведение стандартной библиотеки |
gc.collect() собирает циклы | Гарантия интерфейса, но не времени и не количества |
Строка про немедленное освобождение — самая важная. В CPython del последнее_имя действительно вызывает __del__ сразу, и на этом построена куча кода с файлами и сокетами. Но это свойство подсчёта ссылок, а не языка: на PyPy или Jython тот же код освободит объект когда-нибудь. Именно поэтому существует with.
Со слабой ссылкой ровно та же оговорка, и она есть в самой документации: weakref гарантирует, что ссылка не удерживает объект, но не гарантирует, что после исчезновения последней сильной ссылки она немедленно начнёт возвращать None — до фактического уничтожения объекта она может его ещё отдавать. То, что в примере выше r() вернул None сразу, — снова заслуга подсчёта ссылок, а не обещание библиотеки.
Что стоит помнить
Три цифры из этой статьи устареют — вопрос только в том, какая первой. Полезнее не запомнить их, а запомнить, как проверить: gc.get_threshold() для порогов, sys._debugmallocstats() для аллокатора, sys.getsizeof рядом с sys._is_gil_enabled() для стоимости объекта.
И одно наблюдение напоследок. История с инкрементальным сборщиком — про то, что даже хорошая оптимизация может не пережить встречи с реальной эксплуатацией: её выпустили, она уменьшила паузы, вызвала рост потребления памяти — и её откатили исправляющим выпуском. Между 3.14.3 и 3.14.5 у вас разные сборщики мусора. Ни одна строка вашего кода при этом не изменилась.
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Память в CPython освобождают два механизма, а не один: счётчик ссылок делает это немедленно, циклический сборщик — потом и только для того, до чего счётчик не дотягивается.
- Пороги сборщика
(700, 10, 10), которые цитируют повсеместно, устарели в 3.13. Сейчас(2000, 10, 10), и по дороге они успели побывать(2000, 10, 0). - Арены по 256 КБ, пулы по 4 КБ и 64 класса размеров — описание 32-битной сборки. На 64-битной это 1 МиБ, 16 КиБ и 32 класса, потому что выравнивание там 16 байт, а не 8.
- В сборке без GIL заголовок объекта вырастает с 16 до 32 байт, а pymalloc заменён на mimalloc. Цена видна в
sys.getsizeof— но не у всех объектов, и причина этого сама по себе поучительна.
На самом деле
- Так было до 3.13. Измерено: 3.11 и 3.12 дают
(700, 10, 10), 3.13 —(2000, 10, 10), 3.14.0 —(2000, 10, 0), 3.14.7 — снова(2000, 10, 10). В исходниках на теге v3.12.0 стоит700, на v3.13.0 —2000. Самое неприятное: дефолтные значения никогда не были описаны в документацииgc, поэтому изменение прошло тихо. - Основную работу делает счётчик ссылок, и делает её немедленно: как только
Py_DECREFдоводитob_refcntдо нуля, объект освобождается прямо в этом вызове. Циклический сборщик существует ради единственного случая, который счётчику недоступен, — групп объектов, ссылающихся друг на друга и недостижимых извне. - Наоборот: измерено, что после
delобоих имён счётчики равны единице, а не нулю. Каждый объект честно держит другой. Именно потому, что счётчики ненулевые, механизм подсчёта ссылок считает пару живой — и нужен второй проход, который спрашивает не «есть ли ссылка», а «достижим ли объект». - Это 32-битная сборка. На 64-битной
ARENA_BITS= 20 иPOOL_BITS= 14 (pycore_obmalloc.h, тег v3.14.5), то есть 1 МиБ и 16 КиБ, выравнивание 16 байт, классов размеров 32. Интерпретатор печатает размеры сам:2 arenas * 1048576 bytes/arena,23 unused pools * 16384 bytes— размеры тут константы сборки, а количества меняются от запуска к запуску. Про «64 класса» важно не перепутать: цифра верная именно для классов, потому что на 32-битной сборке выравнивание 8 и 512 ÷ 8 = 64. - Показывает на единицу больше: аргумент функции — тоже ссылка. И размер помехи зависит от способа замера: то же значение, снятое через функцию-помощник, оказывается больше уже на два, потому что параметр этой функции — ещё одна ссылка. Измерено на 3.13.13: на месте
1, через обёртку2. - Имеет, и измеримое. Заголовок объекта вырастает с 16 до 32 байт: вместо счётчика и типа там появляются
ob_tid,ob_mutex,ob_gc_bitsи два счётчика ссылок — локальный и разделяемый.sys.getsizeof(1.5)даёт 24 байта с GIL и 40 без него. У отслеживаемых сборщиком объектов рост при этом не виден: там одновременно исчезаетPyGC_Headна те же 16 байт, поэтому пустой список весит 56 в обеих сборках. Заодно pymalloc заменён на mimalloc — у сборок разный выводsys._debugmallocstats().
Что разобрано
- Зачем это знать?
- Модель в голове
- Счётчик ссылок: главный, а не запасной
- Цикл: единственное, чего счётчик не умеет
- Пороги: число, которое все помнят неправильно
- Где лежат сами объекты: арены, пулы, блоки
- Чем отличить удержанную арену от утечки
- Цена сборки без GIL, измеренная линейкой
- Что здесь гарантия языка, а что — деталь реализации
- Что стоит помнить
- Чем измерено
Унести в работу
- Не приписывать освобождение памяти сборщику: объект стирается в момент, когда
Py_DECREFдоводит счётчик до нуля, а сборщик нужен только для циклов, где счётчики ненулевые. - Не переносить в свою сборку ходовые числа про арены и пулы: 256 КБ и 4 КБ — это 32-битная сборка, а на 64-битной 1 МиБ, 16 КиБ и 32 класса размеров. Интерпретатор печатает их сам.
- Не считать пороги сборщика равными
(700, 10, 10): на 3.13 и 3.14 это(2000, 10, 10). Читать их у себя —gc.get_threshold(). - Не читать
sys.getrefcount(x)как число ссылок в программе: аргумент функции — тоже ссылка, и он показывает на единицу больше. - Переходя на свободнопоточную сборку, закладывать рост памяти: заголовок объекта вырастает с 16 до 32 байт.
Частые заблуждения
«Пороги сборщика — (700, 10, 10)».
Так было до 3.13. Измерено: 3.11 и 3.12 дают (700, 10, 10), 3.13 — (2000, 10, 10), 3.14.0 — (2000, 10, 0), 3.14.7 — снова (2000, 10, 10). В исходниках на теге v3.12.0 стоит 700, на v3.13.0 — 2000. Самое неприятное: дефолтные значения никогда не были описаны в документации gc, поэтому изменение прошло тихо.
«Память в Python освобождает сборщик мусора».
Основную работу делает счётчик ссылок, и делает её немедленно: как только Py_DECREF доводит ob_refcnt до нуля, объект освобождается прямо в этом вызове. Циклический сборщик существует ради единственного случая, который счётчику недоступен, — групп объектов, ссылающихся друг на друга и недостижимых извне.
«Счётчики у объектов в цикле обнуляются, но память всё равно не возвращается — за это и отвечает сборщик».
Наоборот: измерено, что после del обоих имён счётчики равны единице, а не нулю. Каждый объект честно держит другой. Именно потому, что счётчики ненулевые, механизм подсчёта ссылок считает пару живой — и нужен второй проход, который спрашивает не «есть ли ссылка», а «достижим ли объект».
«Арены по 256 КБ, пулы по 4 КБ, 64 класса размеров».
Это 32-битная сборка. На 64-битной ARENA_BITS = 20 и POOL_BITS = 14 (pycore_obmalloc.h, тег v3.14.5), то есть 1 МиБ и 16 КиБ, выравнивание 16 байт, классов размеров 32. Интерпретатор печатает размеры сам: 2 arenas * 1048576 bytes/arena, 23 unused pools * 16384 bytes — размеры тут константы сборки, а количества меняются от запуска к запуску. Про «64 класса» важно не перепутать: цифра верная именно для классов, потому что на 32-битной сборке выравнивание 8 и 512 ÷ 8 = 64.
«sys.getrefcount(x) показывает число ссылок на объект».
Показывает на единицу больше: аргумент функции — тоже ссылка. И размер помехи зависит от способа замера: то же значение, снятое через функцию-помощник, оказывается больше уже на два, потому что параметр этой функции — ещё одна ссылка. Измерено на 3.13.13: на месте 1, через обёртку 2.
«Отключение GIL — это про скорость, к памяти отношения не имеет».
Имеет, и измеримое. Заголовок объекта вырастает с 16 до 32 байт: вместо счётчика и типа там появляются ob_tid, ob_mutex, ob_gc_bits и два счётчика ссылок — локальный и разделяемый. sys.getsizeof(1.5) даёт 24 байта с GIL и 40 без него. У отслеживаемых сборщиком объектов рост при этом не виден: там одновременно исчезает PyGC_Head на те же 16 байт, поэтому пустой список весит 56 в обеих сборках. Заодно pymalloc заменён на mimalloc — у сборок разный вывод sys._debugmallocstats().
Проверка знаний
Объект создан, на него ссылается одно имя. Что вернёт sys.getrefcount(obj), если вызвать его напрямую в той же строке?
Источники и что читать дальше
13 ИСТОЧНИКОВ
- Include/refcount.h — подсчёт ссылок и бессмертные объектыИсходный код CPython. Определения Py_INCREF/Py_DECREF и константы _Py_IMMORTAL_INITIAL_REFCNT (3ULL << 30) — то самое число, которое возвращает getrefcount для None. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/refcount.h
- Include/object.h — раскладка PyObject в обеих сборкахИсходный код CPython. Две разные struct _object: под #ifndef Py_GIL_DISABLED и под #else. Вторая — та самая, где ob_tid, ob_mutex, ob_ref_local и ob_ref_shared. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/object.h
- Include/internal/pycore_obmalloc.h — константы pymallocИсходный код CPython. ARENA_BITS, POOL_BITS, ALIGNMENT и SMALL_REQUEST_THRESHOLD, а также комментарий о том, почему арены берутся через mmap. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_obmalloc.h
- Include/internal/pycore_interp_structs.h — стартовые пороги GCИсходный код CPython. GC_GENERATION_INIT: 2000, 10, 10 для сборки с GIL и отдельная ветка для free-threaded. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_interp_structs.h
- Include/internal/pycore_runtime_init.h (3.12) — пороги до измененияИсходный код CPython. Тот же блок инициализации на теге v3.12.0, где первый порог ещё равен 700. Нужен, чтобы показать, что число менялось. Тег CPython 3.12.0.https://github.com/python/cpython/blob/v3.12.0/Include/internal/pycore_runtime_init.h
- Python/gc.c (3.13) — выбор поколения для сборкиИсходный код CPython. gc_select_generation(): цикл от старшего поколения к младшему и условие long_lived_pending < long_lived_total / 4, из-за которого превышения threshold2 недостаточно для полной сборки. Там же — обнуление счётчиков собранных поколений и инкремент счётчика следующего. Тег CPython 3.13.0.https://github.com/python/cpython/blob/v3.13.0/Python/gc.c
- Include/internal/pycore_gc.h (3.12) — структура поколенияИсходный код CPython. NUM_GENERATIONS равно 3; в struct gc_generation одно поле count с комментарием «count of allocations or collections of younger generations» (число выделений памяти или сборок в более молодых поколениях) — та самая двусмысленность, из-за которой три порога выглядят однородными. Тег CPython 3.12.0.https://github.com/python/cpython/blob/v3.12.0/Include/internal/pycore_gc.h
- Objects/obmalloc.c (3.13) — арены, пулы, блоки и классы размераИсходный код CPython. Вычисление номера класса сдвигом ((nbytes - 1) >> ALIGNMENT_SHIFT), правило «один пул обслуживает один класс» (pool->szidx), список свободных блоков pool->freeblock и условие возврата арены системе — только когда свободны все её пулы. Тег CPython 3.13.0.https://github.com/python/cpython/blob/v3.13.0/Objects/obmalloc.c
- What's New In Python 3.14 — раздел Garbage collectionОфициальная документация. Прямая формулировка о том, что инкрементальный сборщик из 3.14.0–3.14.4 откатили в 3.14.5 из-за сообщений о росте потребления памяти в production.https://docs.python.org/3.14/whatsnew/3.14.html#incremental-garbage-collection
- Документация модуля gc (3.14)Официальная документация. Смысл трёх порогов и пометки «Changed in version 3.14 / 3.14.5» (Изменено в версии 3.14 / 3.14.5). Дефолтные значения здесь не описаны — и никогда не были.https://docs.python.org/3.14/library/gc.html
- Документация модуля weakrefОфициальная документация. Слабая ссылка не удерживает объект; после освобождения вызов ссылки возвращает None.https://docs.python.org/3/library/weakref.html
- PEP 683 — Immortal Objects, Using a Fixed RefcountPEP. Eric Snow и Eddie Elizondo, Python 3.12, статус Final. Откуда взялись объекты с фиксированным счётчиком ссылок.https://peps.python.org/pep-0683/
- PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Sam Gross, Python 3.13, статус Final. Biased reference counting, замена pymalloc на mimalloc, пообъектные блокировки и бессмертие как способ убрать состязание за счётчик.https://peps.python.org/pep-0703/