Программирование PythonПамять и производительностьИнженер по производительности Python

Небольшой объект в графе памяти удерживает большой подграф: почему его retained size может быть огромным?

Небольшой объект в графе памяти удерживает большой подграф: почему его retained size может быть огромным?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Retained size объекта — это объём памяти самого объекта и объектов, которые станут недостижимыми после удаления всех ссылок на него. Поэтому небольшой объект может иметь большой retained size, если через него проходит единственный путь к крупному подграфу. Это не означает, что все объекты под ним принадлежат ему: общий подграф, доступный через другие корни, в его retained size полностью не включается.

Исторический контекст

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

Эта модель особенно полезна для графов объектов с большим количеством ссылок. На практике конкретные профилировщики могут по-разному вычислять retained size, использовать дерево доминаторов или показывать приближённые значения, поэтому результаты нужно трактовать с учётом инструмента.

Постановка проблемы

Если ориентироваться только на shallow size, в отчёте подозрительными окажутся крупные списки, словари и байтовые буферы. Настоящая причина роста памяти может находиться в маленьком объекте, который случайно сохранил ссылку на большой кэш, контекст запроса или корень дерева.

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

Подробное решение

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

Объект A называют доминатором объекта B, если любой путь от корней к B проходит через A. В упрощённом виде retained size A складывается из shallow size A и размеров объектов, которые A доминирует. Если объект доступен ещё и через другой независимый путь, он не должен полностью приписываться A.

Например, маленький объект-обёртка может единственным образом ссылаться на словарь с миллионами записей. Его shallow size будет мал, но retained size — велик, потому что удаление обёртки разорвёт единственный путь к словарю.

Следует отличать retained size от размера всех транзитивно достижимых объектов. Простое суммирование всех объектов по ссылкам часто завышает результат из-за общих объектов и циклов. Кроме того, retained size зависит от набора корней и момента снимка памяти.

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

Ситуация из практики

В веб-сервисе снимок памяти показывает маленький объект контекста запроса с большим retained size. Анализ цепочки ссылок выявляет, что глобальный список отладочных запросов сохраняет этот контекст, а через него доступны большие результаты загрузки файлов.

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

Выбран третий вариант: отладочная структура больше не удерживает граф запроса, а крупные результаты освобождаются после завершения обработки. Это предпочтительнее слабой ссылки, потому что диагностические метаданные должны сохраняться предсказуемо, тогда как сам полный контекст не является обязательным для работы сервиса.

Что кандидаты часто упускают

  1. Всегда ли большой retained size означает утечку памяти?

Нет. Он означает, что объект удерживает большой подграф относительно текущих корней. Это может быть нормальным состоянием, например активный кэш или загруженная модель. Утечкой это становится, когда время жизни объекта или рост удерживаемого подграфа не соответствуют требованиям приложения.

  1. Почему retained size нельзя просто получить суммированием размеров всех объектов, достижимых по ссылкам?

Потому что один объект может быть достижим несколькими путями. Если приписать его каждому владельцу, память будет посчитана несколько раз. Анализ доминаторов пытается выделить часть графа, для которой объект является необходимым посредником, но результат всё равно зависит от корней и правил конкретного профилировщика.

  1. Что нужно искать после обнаружения маленького объекта с большим retained size?

Нужно искать не его собственный размер, а путь удержания от корня и ссылки, которые делают его доминатором крупного подграфа. Затем проверяют владельца этой ссылки: глобальную коллекцию, кэш, очередь, обработчик, замыкание или активную задачу. Исправлять следует жизненный цикл ссылки, а не только удалять отдельные крупные объекты ниже по графу.