Возможность сделать снимок (или дамп) памяти виртуальной машины Java — это инструмент, ценность которого сложно переоценить. Файл дампа содержит копии всех Java объектов, находившихся в памяти в момент снимка. Формат файла хорошо известен, и существует множество инструментов, которые умеют с ним работать.
В моей практике анализ дампов JVM не раз помогал найти причины сложных проблем.
Однако дампы бывают разные. В этот раз передо мной — дамп размером 150 Гб. Моя задача — анализ проблемы, выявленной в процессе, который стал источником этого дампа.
Приложение, в котором я ищу проблему — это гибрид СУБД и системы непрерывной обработки данных. Все данные хранятся в памяти в виде Java объектов, поэтому размер «кучи» может достигать внушительных размеров (личный рекорд — 400 Гб).
Обычно для работы с небольшими дампами я использую JVisualVM. Но полагаю, что дамп такого размера не по зубам ни JVisualVM, ни Eclipse Memory Analyzer, ни другим профайлерам (хотя пробовать я не стал). Даже копирование файла такого объёма с сервера на локальный диск уже представляет проблему.
При анализе дампов в JVisualVM я часто прибегал к возможности использовать JavaScript для программного анализа графа объектов. Графические инструменты хороши, но пролистывать миллионы объектов — не самое приятное занятие. Гораздо приятнее исследовать граф объектов при помощи кода, а не мыши.
Дамп JVM — это всего лишь сериализованный граф объектов; моя задача — извлечь из этого графа конкретную информацию. Мне не очень нужен красивый пользовательский интерфейс: API для работы с графом объектов программным путём — вот инструмент, который на самом деле мне нужен.
Как программно проанализировать дамп «кучи»?
Я начал свое исследование с профайлера NetBeans. NetBeans основан на открытом коде и имеет визуальный анализатор дампа «кучи» (этот же код используется в JVisualVM). Код для работы с дампом JVM является отдельным модулем, а предоставляемый им API вполне подходит для написания собственных специализированных алгоритмов анализа.
Однако у анализатора дампов NetBeans имеется принципиальное ограничение. Библиотека использует временный файл для построения вспомогательного индекса по дампу. Размер индексного файла, как правило, составляет около 25% от размера дампа. Но самое важное — для построения этого файла требуется время, и любой запрос по графу объектов возможен только после того, как индекс построен.
Изучив код, отвечающий за работу с дампом, я решил, что смогу избавиться от необходимости во временном файле, используя более компактную структуру индекса, которую можно хранить в памяти. Мой форк библиотеки, основанной на коде NetBeans профайлера, доступен на GitHub. Некоторые функции API не работают с компактной реализацией индекса (например, обход обратных ссылок), однако для моих задач они не очень нужны.
Еще одним важным изменением по сравнению с исходной библиотекой было добавление HeapPath нотации.
HeapPath — это язык выражений для описания путей в графе объектов, он заимствует некоторые идеи у XPath. Он полезен как в качестве универсального языка предикатов в механизмах обхода графов, так и в качестве простого инструмента для извлечения данных из дампа объектов. HeapPath автоматически конвертирует строки, примитивы и некоторые другие простые типы из структур дампа JVM в обычные Java объекты.
Эта библиотека оказалась очень полезной в нашей повседневной работе. Одним из способов ее применения стал инструмент, анализирующий использование памяти в нашем продукте (гибрид СУБД и системы непрерывной обработки данных), который в автоматическом режиме анализирует размер вспомогательных структур по всем узлам реляционных преобразований (число которых может измеряться сотнями).
Конечно, для интерактивного «свободного поиска» API + Java является не лучшим инструментом. Однако он дает мне возможность делать свою работу, а размер дампа в 150 Гб не оставляет выбора.
Несколько итераций с кодированием на Java и запусками скрипта, анализ результатов — и через пару часов я знаю, что именно сломалось в наших структурах данных. На этом работа с дампом закончена, теперь надо искать проблему в коде.
Кстати: Один проход «кучи» в 150 Гб занимает около 5 минут. Реальный анализ, как правило, требует нескольких проходов, но даже с учётом этого время обработки является приемлемым.
В заключении хочется привести примеры использования моей библиотеки для менее экзотического ПО.
На GitHub есть примеры по анализу дампов JBoss сервера.
Автор: