Как диагностировать рост нативной памяти JVM, если heap dump не показывает виновника?
Используйте Native Memory Tracking (NMT) и сопоставляйте его снимки во времени с размером Java heap и RSS процесса. Если растут категории нативной памяти, например память потоков, Code Cache, Class/Metaspace или внутренние структуры GC, причина находится вне набора обычных Java-объектов и не обязана быть видна в heap dump.
NMT показывает распределение памяти, выделенной самой JVM, но не является универсальным трассировщиком всех нативных библиотек и не заменяет анализ прямых буферов или внешних allocators.
Heap dump изначально решал задачу анализа объектов в Java heap: их типов, ссылок и удерживающих путей. Однако процесс JVM использует и другие области памяти: стеки потоков, метаданные классов, машинный код JIT, структуры сборщика мусора и нативные буферы.
Разделение heap и нативной памяти стало особенно важным в серверных приложениях: RSS мог расти, хотя число Java-объектов оставалось стабильным. NMT появился как диагностический механизм HotSpot для классификации части внутренних нативных аллокаций JVM и сравнения их состояния во времени.
Heap dump не содержит полной картины памяти процесса. Он может показать, что Java-объекты не растут, но не объяснит увеличение памяти стеков новых потоков, JIT-кода, метаданных классов или некоторых нативных буферов.
Без разделения категорий легко ошибочно увеличить -Xmx: это может уменьшить доступную память для остальных компонентов процесса и усилить риск выхода за контейнерный лимит. Обратная ошибка тоже возможна: рост RSS при стабильном heap принять за утечку Java-объектов.
Сначала фиксируют несколько временных точек: размер heap, committed-память, RSS и показатели GC. Затем сравнивают снимки NMT, обращая внимание не только на общий объём, но и на изменение отдельных категорий.
Минимальная последовательность выглядит так:
summary.diff показывает изменения относительно baseline. Для более детального поиска можно использовать режим detail, но он увеличивает накладные расходы и потому обычно нежелателен без необходимости в продакшене.
Типичные признаки имеют разную интерпретацию:
NMT учитывает главным образом аллокации, относящиеся к JVM и инструментируемые HotSpot. Он не обязан полностью показать память, выделенную JNI-кодом, сторонней нативной библиотекой или системным аллокатором. Для таких случаев нужны дополнительные методы: анализ direct memory, метрики библиотек, профилирование native allocations и проверка числа потоков.
Важно различать reserved и committed память. Зарезервированный диапазон ещё не означает физическое потребление; для объяснения RSS нужно смотреть, какие страницы действительно committed и resident, а также учитывать память, не классифицированную NMT.
После обновления сервиса RSS вырос с 2 до 3 ГБ, а live-объём heap после Full GC остался около 700 МБ. Команда рассматривала увеличение -Xmx, поиск утечки в кэше и включение NMT.
Увеличение heap было отвергнуто: оно не объясняло рост RSS и сокращало запас до лимита контейнера. Анализ только heap dump также не помогал, потому что граф объектов не показывал нативные аллокации.
Снимки NMT выявили рост категории Thread после изменения настроек пула: число рабочих потоков увеличилось, а каждый поток резервировал стек. Дополнительная проверка подтвердила рост числа потоков в метриках процесса.
Выбранное решение — ограничить размер пула и устранить дублирующее создание исполнителей. После этого число потоков и RSS стабилизировались. NMT оказался полезен для локализации класса проблемы, но конкретную причину подтвердили метрики приложения и анализ конфигурации пула.
1. Достаточно ли NMT, чтобы найти любую нативную утечку?
Нет. NMT классифицирует значительную часть внутренних аллокаций HotSpot, но не является полным перехватчиком вызовов нативного malloc во всех библиотеках процесса. Утечка в JNI или сторонней библиотеке может почти не отражаться в категориях NMT. В таком случае нужны native-профайлеры, инструменты конкретной библиотеки и анализ границы JNI.
2. Означает ли рост категории Code утечку JIT-кода?
Не обязательно. Рост может отражать нормальную компиляцию новых методов, профилирование и размещение скомпилированных участков в Code Cache. Подозрение возникает, если Code Cache приближается к исчерпанию, компиляции прекращаются или объём продолжает расти аномально после стабилизации нагрузки. Вывод делают по совокупности NMT, логов компиляции и метрик JIT, а не по одному увеличению категории.
3. Почему RSS может быть больше суммы категорий NMT?
NMT не обязан учитывать все страницы процесса: сюда могут входить память нативных библиотек, отображённые файлы, служебные структуры ОС, особенности malloc-арен и фрагментация. Кроме того, reserved-память не равна resident-памяти, а RSS учитывает фактически находящиеся в оперативной памяти страницы процесса. Поэтому разницу исследуют совместно с картой памяти процесса, статистикой ОС и данными о native-компонентах, а не пытаются объяснить только heap dump или NMT.