Deep Engineering
Поиск
Экспертный·Опубликовано·3.12 · 3.13 · 3.14·35 МИН

Управление памятью в CPython: три цифры, которые все повторяют неправильно

Пороги сборщика мусора, размер арены и стоимость объекта — всё это давно не то, что написано в статьях. Каждое число здесь снято с работающего интерпретатора и сверено с исходником на закреплённом теге.

Полное техническое изложение

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 не сообщает, что в соседней сборке ответ отличается вдвое.

Модель в голове

Представьте библиотеку, где у каждой книги на обложке написано, сколько читателей её сейчас держат. Читатель взял — прибавил единицу, вернул — отнял. Как только счётчик обнулился, книга немедленно едет в макулатуру: не в конце дня, не по расписанию, а прямо в этот момент.

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

Всё остальное в этой статье — детали двух этих механизмов и цена, которую за них платят.

Счётчик ссылок: главный, а не запасной

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

Каждый объект начинается с поля ob_refcnt. Макросы Py_INCREF и Py_DECREF его увеличивают и уменьшают, и когда Py_DECREF доводит счётчик до нуля, освобождение происходит прямо там, в этом вызове.

шаг 1 из 6
  1. a = Thing("payload")
  2. b = a
  3. box = [a]
  4. del b
  5. box.clear()
  6. del a

объект создан, на него смотрит одно имя

ob_refcnt

1

кто держит

  • a

измерение мешает измеряемому
Те же данные, снятые двумя способами на одном объекте: sys.getrefcount(x) - 1 на месте дал 1, а через функцию-помощник — 2. Параметр функции тоже ссылка. Поэтому в статье выражение везде записано на месте, а не вынесено в удобный rc().

Рис. 1. Значения счётчика — вывод прогона на CPython 3.13.13, не иллюстрация. Обратите внимание на последний шаг: объект освобождается прямо в момент, когда счётчик обнулился, — это делает не сборщик мусора, а сам механизм подсчёта ссылок.

Обратите внимание на нижнюю плашку рисунка. Она про то, что кажется мелочью, но объясняет половину недоумений вокруг getrefcount: сам замер добавляет ссылку. Аргумент, переданный в функцию, — это ссылка, поэтому sys.getrefcount(x) всегда на единицу больше «настоящего» значения. А если завести удобную обёртку def rc(obj): return sys.getrefcount(obj) - 1, помеха вырастет до двух: параметр obj — ещё одна ссылка. Измерено на 3.13.13: на месте выражение даёт 1, через обёртку — 2.

Вывод не «getrefcount врёт», а более общий: у этого инструмента есть цена наблюдения, и её надо вычитать сознательно.

Слабая ссылка — это ссылка, которая не считается

Иногда объект нужно помнить, но не удерживать: кэш, реестр наблюдателей, обратная ссылка с ребёнка на родителя. Для этого есть weakref — ссылка, которая не увеличивает ob_refcnt.

PYTHON
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.

Цикл: единственное, чего счётчик не умеет

Теперь та самая пара книг с закладками друг на друга.

left = Node('left')
right = Node('right')
leftrightNode('left')ob_refcnt = 1Node('right')ob_refcnt = 1

два обычных объекта, по одной ссылке на каждый

Освобождено:

Рис. 2. Числа сняты на CPython 3.13.13 прогоном с настоящими __del__: после del обоих имён не освободился никто, а gc.collect() вернул 2 и вызвал оба деструктора. Счётчик ссылок здесь не ошибается — он отвечает на свой вопрос правильно. Просто его вопрос («есть ли на объект хоть одна ссылка») не совпадает с нужным («достижим ли объект из работающей программы»).

Числа на рисунке стоит прочитать внимательно, потому что тут обычно и возникает путаница. После del left; del right счётчики не нулевые и не «непонятные» — они равны единице. Каждый объект держит другой, и с точки зрения счётчика это ничем не отличается от живой структуры данных.

Счётчик ссылок здесь не ошибается. Он правильно отвечает на свой вопрос — «есть ли на объект хоть одна ссылка». Просто нужный вопрос другой: «достижим ли объект из работающей программы». Разница между этими двумя вопросами и есть вся причина, по которой в CPython существует второй сборщик.

В замере gc.collect() вернул 2 и вызвал оба __del__. До этого не вызвался ни один.

Пороги: число, которое все помнят неправильно

Сборщик циклов не работает постоянно. Он просыпается, когда разница между числом созданных и числом уничтоженных отслеживаемых объектов превысила порог.

Что именно считают эти три числа

Прежде чем разбираться, какие числа правильные, стоит понять, что они настраивают: без этого (700, 10, 10) выглядит как три однородных значения, а это не так.

Поколений три, и это три списка. Каждый отслеживаемый объект лежит ровно в одном из них. Только что созданный попадает в нулевое. Тот, кто пережил сборку своего поколения, переезжает в следующее — и попадает под сборку реже.

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

Теперь главное, из-за чего три числа кажутся однородными. Они меряют разное:

ПорогЧто считает его счётчикКогда счётчик растёт
threshold0объектыпри каждом создании отслеживаемого объекта
threshold1сборки поколения 0каждый раз, когда собрали нулевое поколение
threshold2сборки поколения 1каждый раз, когда собрали первое

То есть (2000, 10, 10) читается так: собрать нулевое поколение после 2000 новых объектов; каждую десятую такую сборку заодно собрать первое; каждую десятую сборку первого — заодно второе. Первое число — про объекты, два других — про сборки. Отсюда и порядок величин: 2000 против 10 — это не «один порог намного больше», это разные единицы измерения.

Двусмысленность зашита в сам исходник — в структуре поколения одно поле count обслуживает оба случая, и комментарий к нему честно перечисляет оба:

C
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:

C
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, сборщик обходит его вместе со всеми младшими, обнуляет их счётчики и добавляет единицу к счётчику следующего:

C
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.5(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() оно есть всегда — и отвечает за текущую сборку.

Проверить действующий порог можно за десять строк:

PYTHON
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.

What's New In Python 3.14, раздел Garbage collection

Дословно: версии 3.14.0–3.14.4 вышли с новым инкрементальным сборщиком, но из-за ряда сообщений о существенном росте потребления памяти в production его вернули к поколенческому сборщику из 3.13. Запись в журнале изменений 3.14.5rc1 (gh-142516) уточняет, что откат сделан для сборки по умолчанию, а сборщик free-threaded сборки оставлен без изменений. Это не значит, что там остался инкрементальный: у сборки без 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-битной сборки, и число классов зависит от неё напрямую. На 32-битной выравнивание равно восьми, и классов там 64. Дальше в статье это разобрано подробнее; здесь важно только, что «32» — не свойство pymalloc вообще, а свойство сборки, на которой сняты все числа этой статьи.

В исходнике вычисление класса — одна строка арифметики, номер получается сдвигом:

C
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;

Ключевое — правило, ради которого всё построено: один пул обслуживает ровно один класс. Все блоки внутри пула одинакового размера, а значит взаимозаменяемы. И это превращает выделение памяти в две операции над односвязным списком:

  • выделить — снять первый блок со списка свободных этого пула;
  • освободить — положить блок обратно в начало списка.

Ни поиска подходящей дырки, ни разрезания большого куска на меньший, ни склейки соседних свободных участков — всего того, чем занят обычный malloc общего назначения. Он вынужден этим заниматься именно потому, что у него запросы любого размера вперемешку. pymalloc платит за отказ от этой работы округлением: часть блока пропадает впустую, зато выделение стоит несколько инструкций. Сколько именно пропадает, интерпретатор считает сам — цифра будет ниже, в разборе вывода sys._debugmallocstats().

Поэтому ответ на «почему классы разные» — чтобы внутри одного пула всё было одинаковым. Разнообразие вынесено на уровень выше: пулов много, и каждый занят своим размером.

Как память заполняется и как освобождается

Заполнение идёт сверху вниз и только по необходимости:

  1. Программа просит 100 байт. Округление вверх даёт класс 112 байт.
  2. Аллокатор ищет пул этого класса, в котором есть свободный блок, и снимает блок со списка свободных.
  3. Если такого пула нет — берётся пустой пул (из уже имеющейся арены) и назначается классу 112. Пул не рождается со своим размером, он его получает при первом использовании.
  4. Если пустых пулов не осталось — у операционной системы запрашивается новая арена в 1 МиБ, и она нарезается на 64 пула.

Освобождение идёт теми же тремя уровнями, но каждый следующий уровень требует более сильного условия:

Что освобождаетсяУсловиеКуда возвращается
блокобъект удалёнв список свободных своего пула — мгновенно, без участия ОС
пулосвободились все его блокив запас пустых пулов арены, может быть переназначен другому классу
аренаосвободились все её пулыоперационной системе

Вот эта асимметрия и есть причина репутации «Python не отдаёт память». Вниз память идёт мгновенно и по одному блоку, вверх — только целыми уровнями и только при полной пустоте. Один живой объект удерживает свой пул; один непустой пул удерживает арену в мегабайт.

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

запрошено
100 Б
класс 6
112 Б

Запрос округляется вверх до кратного 16. Блок будет на 12 Б больше, чем просили — это плата за то, что все блоки в пуле одинаковы.

Рис. 4. Схема алгоритма, а не запись настоящей кучи. Измерены здесь только константы: арена 1 048 576 Б, пул 16 384 Б (64 пула в арене), 32 класса с шагом 16 Б до 512 Б включительно — вывод sys._debugmallocstats(), одинаковый на 3.11.15, 3.12.3 и 3.13.13. Порядок выдачи блоков и их количество на рисунке упрощены: в настоящем пуле на 64 Б помещается 255 блоков, а к моменту запуска вашего кода интерпретатор уже держит тысячи занятых.

Арена — непрерывный кусок адресного пространства, который интерпретатор берёт у операционной системы целиком. На 64-битной сборке её размер 1 048 576 байт, то есть 1 МиБ, и она нарезается ровно на 64 пулов.

64 пула по 16 384 байт. Заполненность на схеме — иллюстративная раскладка; точные числа занятости смотрите на уровне «Блок», они измерены.

Рис. 3. Числа — разбор вывода sys._debugmallocstats() на CPython 3.13.13 в конце прогона измерительного скрипта. Размеры и число классов — константы сборки, занятость — снимок этого процесса. Константы сверены с Include/internal/pycore_obmalloc.h на теге v3.14.5: ARENA_BITS 20, POOL_BITS 14, ALIGNMENT 16, SMALL_REQUEST_THRESHOLD 512. Если вы помните «256 КБ и 4 КБ» — это верно для 32-битной сборки, где те же константы равны 18 и 12.

Здесь и живёт вторая устаревшая цифра. Классическое описание — «арены по 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 байта. А арена возвращается операционной системе, только когда полностью опустела, — да и то не всегда: последнюю свободную арену аллокатор придерживает, чтобы не отдавать и тут же просить её обратно. Поэтому долгоживущий процесс, однажды создавший миллион объектов и удаливший их, может не вернуть память: достаточно, чтобы в каждой арене остался один живой объект.

Цена free-threading, измеренная линейкой

Теперь то, что в 2026 году нельзя опускать в статье про память.

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 с GIL3.14t без GIL
object()16 Б32 Б
1.524 Б40 Б
пустой dict64 Б64 Б
пустой list56 Б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 с GIL3.14t без GIL
getrefcount(None)32212254723221225472
getrefcount(257)33221225472

Число 257 — не малое целое, в обычной сборке это самый обыкновенный объект со счётчиком 3. В сборке без GIL записанный в коде литерал 257 оказывается бессмертным.

Формулировать это надо аккуратно, иначе получится неправда. Бессмертно не «число 257», а константа кодового объекта: та же семёрка, полученная вычислением, ведёт себя обычно.

PYTHON
sys.getrefcount(257)          # 3221225472 — литерал из кода
a = 256; b = a + 1
sys.getrefcount(b)            # 2 — то же значение, обычный объект

Вывод снят на 3.14.0rc2t. Причина в том, что free-threaded сборка интернирует и делает бессмертными большинство констант кодовых объектов (Objects/codeobject.c, функция should_immortalize_constant) — так снимается состязание за их счётчик между потоками. Общая логика та же, что в PEP 703: бессмертие убирает атомарные операции над счётчиком. Но список объектов, которые PEP объявляет бессмертными, — это набор из 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() для стоимости объекта.

И одно наблюдение напоследок. История с инкрементальным сборщиком — про то, что даже хорошая оптимизация может не пережить встречи с production: её выпустили, она уменьшила паузы, вызвала рост потребления памяти и была откачена в патч-релизе. Между 3.14.3 и 3.14.5 у вас разные сборщики мусора. Ни одна строка вашего кода при этом не изменилась.

Частые заблуждения

Утверждение

«Пороги сборщика — (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.5 — снова (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().

Проверка знаний

Вопрос 1 из 5

Объект создан, на него ссылается одно имя. Что вернёт sys.getrefcount(obj), если вызвать его напрямую в той же строке?

Источники и что читать дальше

13 ИСТОЧНИКОВ

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. Документация модуля gc (3.14)Официальная документация. Смысл трёх порогов и пометки «Changed in version 3.14 / 3.14.5». Дефолтные значения здесь не описаны — и никогда не были.https://docs.python.org/3.14/library/gc.html
  11. Документация модуля weakrefОфициальная документация. Слабая ссылка не удерживает объект; после освобождения вызов ссылки возвращает None.https://docs.python.org/3/library/weakref.html
  12. PEP 683 — Immortal Objects, Using a Fixed RefcountPEP. Eric Snow и Eddie Elizondo, Python 3.12, статус Final. Откуда взялись объекты с фиксированным счётчиком ссылок.https://peps.python.org/pep-0683/
  13. 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/