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

В дампе heap объект занимает 32 байта, но его retained size составляет 2 ГБ. Какой вывод следует сделать?

В дампе heap объект занимает 32 байта, но его retained size составляет 2 ГБ. Какой вывод следует сделать?

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

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

Shallow size объекта — это память, занятая самим объектом, а retained size — приблизительный объём памяти, который перестал бы быть достижимым после удаления этого объекта и зависящих от него объектов. Поэтому объект размером 32 байта может удерживать 2 ГБ через поле-ссылку, цепочку объектов или структуру данных. Это не означает, что сам объект занимает 2 ГБ или что удаление ссылки гарантированно немедленно освободит весь указанный объём.

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

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

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

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

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

Неверная интерпретация опасна в обе стороны: можно начать оптимизировать мелкие объекты вместо причины удержания или удалить «подозрительную» ссылку, не проверив, что часть графа доступна через другие пути. Кроме того, retained size в дампе — это результат анализа конкретного снимка, а не доказательство фактического освобождения памяти после изменения программы.

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

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

Retained size обычно оценивается через доминирование. Объект A доминирует над объектом B, если любой путь от корней GC до B проходит через A. В retained size A включают размер A и объектов, которые он доминирует.

На практике объект в 32 байта может владеть ссылкой на большую HashMap, дерево или цепочку буферов. Если к этим данным нет другого пути от GC Roots, анализатор покажет большой retained size у маленького владельца.

Показатель зависит от состояния heap в момент создания дампа и от алгоритма инструмента. Если существуют альтернативные пути от GC Roots, соответствующие объекты не будут полностью отнесены к одному владельцу; часть памяти может быть разделена между несколькими кандидатами. Поэтому нужно исследовать пути от GC Roots, dominator tree и типы удерживающих ссылок, а не делать вывод только по одному числу.

Также retained size не равен гарантированному объёму, который процесс немедленно вернёт операционной системе. После устранения удерживающей ссылки объекты сначала должны стать недостижимыми и быть обработаны сборщиком мусора, а освобождённая память может остаться внутри heap или нативных структур JVM.

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

После нескольких часов работы сервиса heap dump показал небольшой объект контекста запроса с retained size около 2 ГБ. Проверка путей от GC Roots выявила, что контекст оставался в таблице подписчиков после завершения запроса и через одну ссылку удерживал коллекцию результатов.

Рассматривались два варианта. Увеличение heap давало временный запас, но не устраняло удержание и увеличивало стоимость GC. Принудительное ограничение размера кэша уменьшало ущерб, но скрывало ошибку жизненного цикла и могло приводить к потере полезных данных.

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

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

1. Означает ли retained size 2 ГБ, что после удаления объекта освободится ровно 2 ГБ?

Нет. Это оценка для конкретного графа достижимости и снимка heap. Часть объектов может иметь другие пути от GC Roots, а освобождение произойдёт только после того, как объекты станут недостижимыми и сборщик обработает их. Кроме того, JVM не обязана вернуть соответствующую память операционной системе.

2. Почему два объекта могут иметь одинаковый большой retained size?

Они могут быть связаны с общими объектами или находиться на разных уровнях одного графа. Анализатор распределяет shared-объекты по правилам дерева доминирования, поэтому значения нельзя складывать без проверки пересечений. Для поиска владельца нужно смотреть dominator tree и цепочки удержания, а не сортировку объектов по одному показателю.

3. Чем поиск объекта с большим retained size отличается от поиска самой крупной коллекции?

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