Общепринятый подход к решению проблем в программном коде состоит в добавлении отладочных операторов для считывания содержимого полей в объекте или даже во всех наборах данных. Во многих случаях разработчику приходится делать это итеративно, по мере того как он обнаруживает, что для понимания и решения проблемы ему требуется все больше информации. Этот процесс может быть вполне эффективным, однако иногда он не срабатывает: введение отладочного кода может привести к исчезновению проблемы; у разработчика может возникнуть необходимость в добавлении отладочных операторов к программному коду, который ему не принадлежит; отладка может потребовать от разработчика перезапуска процессов; воздействие отладочных операторов на общую производительность может воспрепятствовать работе отлаживаемого приложения.
Memory Analyzer — это межплатформенный инструмент с открытым исходным кодом, который можно использовать не только для диагностирования проблемы с памятью, но и для углубленного понимания состояния и поведения всего Java-приложения. Memory Analyzer читает т. н. «мгновенный дамп» (snapshot dump), созданный средой исполнения Java в процессе работы приложения, что предоставляет разработчику способ диагностирования замысловатых проблем, которые отладочный код может и не выявить.
Данная статья показывает, как генерировать дампы и как использовать их для исследования и диагностирования состояния приложения. Memory Analyzer позволяет исследовать потоки, объекты, пакеты и целые совокупности данных с целью устранения проблем Java-кода, не ограничивающихся только утечками памяти.
На данный момент инструмент Memory Analyzer способен работать со следующими тремя типами дампов.
- IBM Portable Heap Dump (PHD). Этот дамп, представленный в закрытом формате IBM, содержит только тип/размер каждого Java-объекта в процессе, и отношения между объектами. Файл дампа в этом формате значительно меньше, чем в других форматах, и содержит наименьшее количество информации. Тем не менее этих данных обычно достаточно для диагностирования утечек памяти и для получения базового представления об архитектуре приложения и занимаемом им пространстве.
- Бинарный дамп HPROF. Двоичный формат HPROF содержит все данные, присутствующие в формате IBM PHD, а также данные примитивов, содержащиеся внутри Java-объектов, и подробности потоков. Этот формат позволяет посмотреть на значения, содержащиеся в полях внутри объектов, и увидеть, какие методы выполнялись в момент совершения анализируемого дампа. Дополнительные данные примитивов значительно увеличивают размеры дампов в формате HPROF по сравнению с дампами в формате PHD; HPROF-дамп имеет приблизительно такой же размер, как используемая куча Java.
- Системные дампы IBM. В тех случаях, когда используется среда исполнения Java от IBM, в инструмент Memory Analyzer может быть загружен собственный («нативный») файл дампа операционной системы — дамп памяти ядра на AIX® или Linux, малый дамп памяти на Windows® или дамп SVC на z/OS®. Эти дампы содержат образ всей памяти исполняемого приложения — всю информацию и все данные в формате HPROF, а также всю нативную память и всю информацию потоков. Файлы дампов в этом формате имеют самые большие размеры и содержат наиболее полную информацию.
Оба типа дампов от IBM доступны только при установленном плагине DTFJ (Diagnostic Tool Framework for Java) (см. раздел Ресурсы и врезку Варианты инструмента Memory Analyzer.
В таблице 1 обобщены различия между разными типами файлов дампов.
Таблица 1. Сводные характеристики по типам дампов
| Формат дампа | Приблизительный размер на диске | Объекты, классы и загрузчики классов (classloader) | Подробности потоков | Имена полей | Ссылки на поля и на массивы | Поля примитивов | Содержание массива примитивов | Точные корни для сборки мусора | Нативная память и потоки |
|---|---|---|---|---|---|---|---|---|---|
| IBM PHD | 20% от размера кучи Java | Да | Совместно с Javacore* | Нет | Да | Нет | Нет | Нет | Нет |
| HPROF | Размер как у кучи Java | Да | Да | Да | Да | Да | Да | Да | Нет |
| Системные дампы IBM | Размер на 30% больше, чему у кучи Java | Да | Да | Да | Да | Да | Да | Да | Да |
*При загрузке двух одновременно сгенерированных файлов — файла javacore.txt (файл дампа потока IBM) и файла heapdump.phd — инструмент Memory Analyzer обеспечивает доступ к подробностям потока в дампе формата IBM PHD.
И формат HPROF, и форматы системных дампов IBM поддерживают высокую степень сжатия — как правило, с помощью инструментов операционной системы соответствующие дампы могут быть сжаты до приблизительно 20% от своего первоначального размера.
Для каждой из сред исполнения Java имеются различные механизмы получения разнообразных дампов. Это предоставляет гибкие возможности создания мгновенных дампов для сценариев, которые включают не только ошибки типа OutOfMemoryError. Перечень доступных механизмов зависит от того, среду исполнения Java от какого поставщика использует разработчик.
Вне зависимости от типа дампа необходимо гарантировать для дампов достаточное дисковое пространство, чтобы они не подвергались усечению. По умолчанию дампы помещаются в текущий рабочий каталог процесса JVM. Для JVM-машины IBM это местоположение можно изменить с помощью опции командной строки -Xdump. Для JVM-машины HotSpot это местоположение можно изменить с помощью опции командной строки -XX:HeapDumpPath. Ссылки на соответствующий синтаксис приведены в разделе Ресурсы.
Дампы, сделанные с помощью операционной системы, могут быть использованы как для JVM-машины от IBM (IBM JVM), так и для JVM-машины HotSpot (JVM HotSpot). Для IBM JVM можно создавать дампы с помощью инструмента jextract (поставляемого вместе с JDK) и загружать их непосредственно в Memory Analyzer; для JVM HotSpot следует использовать инструмент jmap для извлечения дампов кучи (heap dump) из дампов ядра (core dump). Позднее мы подробно рассмотрим оба этих метода. Однако в некоторых операционных системах перед созданием дампа памяти необходимо гарантировать, что процесс работает с достаточным значением ulimit; в противном случае дамп памяти будет усеченным, а возможности анализа будут ограниченными. При некорректных значениях ulimit необходимо сначала их изменить, а затем перезапустить процесс и только после этого приступать к сборке дампа. В разделе Ресурсы приведены ссылки на подробную информацию о получении системных дампов при использовании операционных систем AIX, Linux®, z/OS и Solaris.
Получение мгновенного дампа: среды исполнения HotSpot
Среды исполнения Java на основе HotSpot генерируют только дамп в формате HPROF. В распоряжении разработчика имеется несколько интерактивных методов генерации дампов и один метод генерации дампов на основе событий.
- Интерактивные методы:
Использование
Ctrl+Break. : Если для исполняющегося приложения установлена опция командной строки-XX:+HeapDumpOnCtrlBreak, то дамп формата HPROF генерируется вместе с дампом потока при наступлении событияCtrl+BreakилиSIGQUIT(обычно генерируется с помощьюkill -3), которое инициируется посредством консоли. Эта опция может быть недоступна в некоторых версиях. В этом случае попытайтесь использовать следующую опцию:-Xrunhprof:format=b,file=heapdump.hprof
Использование инструмента
jmap. : Утилитаjmap(см. раздел Ресурсы), поставляемая в составе каталога bin комплекта JDK, позволяет запрашивать дамп HPROF из исполняющегося процесса. В случае Java 5 используйте следующий синтаксис:jmap -dump:format=b pid
В случае Java 6 используйте следующую версию, где
live— это опция, в результате которой в файл дампа процесса с идентификатором ID (PID) записываются только «живые» объекты:jmap -dump[live,]format=b,file=filename pid
Использование операционной системы. : Для создания файла ядра воспользуйтесь неразрушающей командой
gcoreили разрушающими командамиkill -6илиkill -11. Затем извлеките дамп кучи из файла ядра с помощью утилитыjmap:jmap -dump:format=b,file=heap.hprof path to java executable core
- Использование инструмента JConsole. Операция
dumpHeapпредоставляется в JConsole как MBean-компонентHotSpotDiagnostic. Эта операция запрашивает генерацию дампа в формате HPROF.
- Метод на основе событий
- Событие
OutOfMemoryError: Если для исполняющегося приложения установлена опция командной строки-XX:+HeapDumpOnOutOfMemoryError, то при возникновении ошибкиOutOfMemoryErrorгенерируется дамп формата HPROF. Это идеальный метод для «производственных» систем, поскольку он практически обязателен для диагностирования проблем памяти и не сопровождается постоянными накладными расходами с точки зрения производительности. В старых выпусках сред исполнения Java на базе HotSpot для этого события не устанавливается предельное количество дампов кучи в пересчете на одну JVM; в более новых выпусках допускается не более одного дампа кучи для этого события на каждый запуск JVM.
- Событие
Получение мгновенного дампа: среды исполнения IBM
Среды исполнения IBM предоставляют механизмы создания дампов и трассировки, которые позволяют генерировать дампы в формате PHD или системные дампы в большом количестве интерактивных и основанных на событиях сценариев. Кроме того, генерировать интерактивные дампы можно с помощью инструмента Health Center или программным способом — с помощью API-интерфейса Java.
- Интерактивные методы.
Использование
SIGQUITили Ctrl+Break: Когда в среду исполнения IBM посылается команда Ctrl+Break илиSIGQUIT(обычно генерируется с помощьюkill -3), в механизме дампа IBM генерируется пользовательское событие. По умолчанию это событие генерирует только файл дампа потока (javacore.txt). Можно использовать опцию-Xdump:heap:events=userдля генерации дампа в формате PHD или опцию-Xdump:system:events=userдля генерации системного дампа Java-приложения.Использование операционной системы для создания системного дампа
- AIX: команда
gencore(или разрушающая командаkill-6илиkill-11) - Linux/Solaris: команда
gencore (или разрушающая командаkill-6илиkill-11) - Windows:
userdump.exe - z/OS:
SVCDUMPили консольный дамп
- AIX: команда
Использование инструмента IBM Monitoring and Diagnostics Tools for Java — Health Center. В меню инструмента Health Center есть опция для запроса дампа в формате PHD или системного дампа из рабочего процесса Java (см. раздел Ресурсы).
- Методы на основе событий. Механизмы IBM для дампов и трассировки предоставляют гибкий набор возможностей по генерации PHD-дампов и системных дампов для большого числа событий – от выдаваемых исключений до выполняемых методов. Это позволяет генерировать дампы для большинства сценариев проявления проблем, которые необходимо диагностировать:
Использование механизма IBM для генерации дампов. Механизм генерации дампов предоставляет большое количество событий, при наступлении которых можно генерировать PHD-дамп или системный дамп. Кроме того, он позволяет осуществлять фильтрацию по типам этих событий для более детального управления выбором момента генерации дампа.
Использование опции
-Xdump:whatпозволяет увидеть события по умолчанию. Например, можно увидеть, что файлы heapdump.phd и javacore.txt создаются для первых четырех исключений типаOutOfMemoryErrorв JVM.Чтобы собрать больше данных по исключениям
OutOfMemoryError, можно вместо дампа кучи генерировать системный дамп:-Xdump:heap:none -Xdump:java+system:events=systhrow, filter=java/lang/OutOfMemoryError,range=1..4,request=exclusive+compact+prepwalk
Некоторые исключения, например,
NullPointerException, повсеместно генерируются в самых разных приложениях и в самом разнообразном коде. Это затрудняет генерацию дампа по конкретному интересующему вас исключениюNullPointerException. Чтобы конкретизировать исключение, по которому следует генерировать дамп, для событий throw (выдать) и catch (перехватить) предоставляется дополнительный уровень фильтрации, позволяющий специфицировать методы выдачи и перехвата исключений соответственно. Эта задача решается посредством добавления разделителя#, за которым следует метод выдачи и перехвата. Например, эта опция генерирует системный дамп, если исключениеNullPointerExceptionвыдал методbad():-Xdump:system:events=throw, filter=java/lang/NullPointerException#com/ibm/example/Example.bad
Следующая опция генерирует системный дамп в том случае, когда метод
catch()перехватил исключениеNullPointerException:-Xdump:system:events=catch, filter=java/lang/NullPointerException#com/ibm/example/Example.catch
В дополнение к фильтрации событий можно специфицировать диапазон событий, по которым следует генерировать дампы. Например, эта опция генерирует дамп только при пятом появлении исключения
NullPointerException:-Xdump:system:events=throw, filter=java/lang/NullPointerException,range=5
Следующая опция задает диапазон, чтобы генерировать дамп только при втором, третьем и четвертом появлении исключения
NullPointerException:-Xdump:system:events=throw, filter=java/lang/NullPointerException,range=2..4
Наиболее полезные события и фильтры обобщены в таблице 2
Таблица 2. Доступные события для генерации дамповСобытие Описание Доступная фильтрация Пример gpfОбщее нарушение защиты (аварийный отказ) -Xdump:system:events=gpfuserСигнал, генерированный пользователем ( SIGQUITили Ctrl+Break)-Xdump:system:events=uservmstopЗавершение работы VM, включая вызов System.exit()По коду завершения -Xdump:system:events=vmstop,filter=#0..#10
Генерация системного дампа при завершении работы VM с кодом завершения в диапазоне0и10.loadЗагрузка класса Имя класса -Xdump:system:events=load,filter=com/ibm/example/Example
Генерация системного дампа в случае загрузки классаcom.ibm.example.Example.unloadВыгрузка класса По имени класса -Xdump:system:events=unload,filter=com/ibm/example/Example
Генерация системного дампа в случае выгрузки классаcom.ibm.example.Example.throwБросание исключения По имени класса с исключением -Xdump:system:events=throw,filter=java/net/ConnectException
Генерация системного дампа в случае генерации исключенияConnectException.catchПоимка исключения По имени класса с исключением -Xdump:system:events=catch,filter=java/net/ConnectException
Генерация системного дампа в случае поимки исключенияConnectException.systhrowJVM собирается бросить Java-исключение (эта ситуация отличается от события throw, поскольку она инициируется только для ошибок, обнаруженных внутри JVM)По имени класса с исключением -Xdump:system:events=systhrow,filter=java/lang/OutOfMemoryError
Генерация системного дампа в случае генерации ошибкиOutOfMemoryError.allocationВыделение Java-объекта По размеру выделяемого объекта -Xdump:system:events=allocate,filter=#5m
Генерация системного дампа в случае выделения объекта размером более 5 МБ.
- Использование механизма трассировки IBM.
Механизм трассировки позволяет инициировать PHD-дампы и системные дампы для любого Java-метода, работающего в приложении, при входе в метод или выходе из метода. Эта задача решается посредством использования ключевого слова trigger в опциях командной строки
-Xtrace, которые управляют механизмом трассировки IBM. Синтаксис для опции инициирования:method{methods[,entryAction[,exitAction[,delayCount[,matchcount]]]]}
Добавление следующей опции командной строки к приложению генерирует системный дамп в случае вызова метода
Example.trigger ():-Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,sysdump}
Следующая опция командной строки генерирует PHD-дамп в случае вызова метода
Example.trigger ():-Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,heapdump}
Тем не менее рекомендуется задавать диапазон таким образом, чтобы дампы создавались не при каждом вызове соответствующего метода. В следующем примере первые пять вызовов
Example.trigger()игнорируются, а затем инициируется один дамп:-Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,sysdump,,5,1}
Обратите внимание, что в этом примере дляexitActionиспользуется пустой терм, поскольку мы инициируем дампы только по входу в метод.
- Программируемые методы:
Кроме того, среды исполнения IBM предоставляет класс
com.ibm.jvm.Dumpс методамиjavaDump (),heapDump ()иsystemDump (). Эти методы генерируют дампы потока, PHD-дампы и системные дампы, соответственно.
Получение дампа с помощью инструмента Memory Analyzer
В дополнение к методам для получения дампов, которые предоставляются самими средами исполнения, инструмент Memory Analyzer поддерживает функцию Acquire Heap Dump (см. рис. 1), которая позволяет инициировать и загрузить мгновенный дамп из Java-процесса, исполняющегося на той же машине, что и Memory Analyzer:
Рис. 1. Функция Acquire Heap Dump инструмента Memory Analyzer
В средах исполнения на базе HotSpot инструмент Memory Analyzer генерирует дампы с помощью jmap. В средах исполнения IBM дамп генерируется с помощью функциональности Java под названием late attach и программируемого API-интерфейса. Для работы этой функции требуется выпуск Java 6 SR6 (предшествующие выпуски не содержат функции late attach).
В случае системных дампов IBM каждый дамп необходимо подвергнуть постобработке с помощью инструмента jextract, поставляемого в составе JDK:
jextract core |
В идеальном случае инструмент jextract выполняется на той же физической машине, которая произвела дамп; используется инструмент jextract из той же установки JDK, которая произвела дамп; доступ по чтению имеется к тем же библиотекам, с которыми исполнялся процесс java. Необходимо учитывать, что при обработке дампа инструмент jextract может потреблять значительные процессорные ресурсы, что может быть недопустимо во многих производственных системах. В этом случае следует обрабатывать дамп на наиболее близкой по характеристикам системе, например, на системе предпроизводственного тестирования. Версии сред исполнения Java Service Refresh (SR) и Fix Pack (FP) на этих системах должны совпадать.
Инструмент jextract создает ZIP-файл, который содержит исходный дамп ядра, обработанное представление этого дампа, исполняемую Java-программу и библиотеки, использовавшиеся процессом java. После завершения работы инструмента jextract можно удалить исходный (неархивированный) дамп ядра. Результирующий ZIP-файл следует загрузить в Memory Analyzer.
Из системного дампа, обработанного инструментом jextract, можно извлечь PHD-дамп. Для этого следует загрузить результирующий ZIP-файл в инструмент jdmpview и выполнить команду heapdump (см. раздел Ресурсы).
Использование инструмента Memory Analyzer для анализа проблем
Инструмент Memory Analyzer способен диагностировать ошибки типа OutOfMemoryError посредством отыскания в приложении областей, имеющих утечку памяти или требующих пространства больше, чем имеется доступной памяти. Memory Analyzer автоматически обнаруживает утечки и генерирует отчет Leak Suspects (см. раздел Ресурсы).
Кроме того, дополнительные данные, доступные в HPROF-дампах и в системных дампах IBM, особенно имена полей и значения полей, в сочетании с возможностями представления Inspector и языка OQL (Object Query Language) позволяют диагностировать более широкий диапазон проблем, а не только выявлять проблемы типа «Чем занята вся память?». Например, можно выяснить занятость и загруженность коллекций, чтобы увидеть, насколько эффективно заданы их размеры, или установить имя хоста и порт, ассоциированные с исключением ConnectException, чтобы увидеть, какое соединение пыталось установить исследуемое приложение.
Рассмотрение полей в объекте с помощью представления Inspector
Когда какой-либо объект выбран в инструменте Memory Analyzer, представление Inspector показывает имеющуюся информацию по этому объекту, включая иерархию классов, атрибуты и «статику». Панель Attributes показывает поля экземпляра и значения, связанные с объектом, а панель Statics показывает статические поля и значения, ассоциированные с классом.
На рис. 2 показано представление Inspector для простого объекта java.net.URL, которое позволяет увидеть подробности об этом объекте, включая тип протокола для URL и место назначения:
Рис. 2. Панели Statics, Attributes и Class Hierarchy в представлении Inspector
В панели Attributes на рисунке 2, можно увидеть, что объект URL ссылается на файл JAR (поле protocol), находящийся в локальной файловой системе (его местоположение специфицируется полями path и file).
Выполнение запросов к объектам с помощью языка OQL
Для исследования дампа с помощью специально создаваемых SQL-подобных запросов можно использовать язык OQL. Этой тема заслуживает отдельной статьи, поэтому мы ограничимся несколькими примерами. Для получения дополнительной информации обратитесь к справочной системе (Help) инструмента Memory Analyzer и перейдите в раздел, посвященный языку OQL.
OQL особенно полезен для отслеживания исходящих ссылок и полей набора объектов до определенного поля. Например, если у класса A есть поле foo типа B, а у класса B есть поле bar, которое имеет тип String, то простой запрос для отыскания всех объектов String будет иметь следующий вид:
SELECT aliasA.foo.bar.toString() FROM A aliasA |
Мы даем классу A псевдоним aliasA, на который затем ссылаемся в операторе SELECT. Этот запрос выбирает исключительно из экземпляров класса A. Если бы мы хотели выбирать из всех экземпляров класса A, а также из любых подклассов, мы бы использовали следующий запрос:
SELECT aliasA.foo.bar.toString() FROM INSTANCEOF A aliasA |
Рассмотрим более сложный пример с несколькими DirectByteBuffer:
SELECT k, k.capacity FROM java.nio.DirectByteBuffer k WHERE ((k.viewedBuffer=null)and(inbounds(k).length>1)) |
В данном случае мы хотим получить поле емкости любого буфера DirectByteBuffer, которое содержит сведения о нативной памяти, удерживаемой соответствующим объектом. Мы также хотим отфильтровать все буферы DirectByteBuffer, у которых поле viewedBuffer содержит значение null (поскольку это только представления других буферов DirectByteBuffer), и которые имеют более одной входящей ссылки (чтобы не видеть буферов с фантомными ссылками, ожидающими очистки, поскольку нам нужны только «живые» буферы DirectByteBuffer).
Выполнение сравнений между представлениями или между дампами
Memory Analyzer позволяет сравнивать таблицы, сгенерированные запросами. Эти таблицы могут быть из одного дампа (что позволяет увидеть, присутствуют ли объекты String из одного представления в объекте, являющемся элементом какой-либо коллекции, которая видна в другом представлении) или из разных дампов (что позволяет искать изменения в данных, например, рост количества коллекций).
Чтобы выполнить сравнение, нужно добавить соответствующие таблицы в Compare Basket (корзина для сравнения), а затем запросить сравнение записей в этой «корзине». Сначала найдите и выберите запись для нужной таблицы в представлении Navigation History, а затем в контекстном меню выберите опцию Add to Compare Basket (добавить в корзину) (см. рис. 3).
Рис. 3. Добавление таблиц из представления Navigation History в Compare Basket
После того, как в Compare Basket будут присутствовать две записи, можно выполнить сравнение с помощью кнопки Compare-the-results (красный восклицательный знак) в правом верхнем углу панели (см. рис. 4).
Рис. 4. Сравнение записей, содержащихся в Compare BasketBasket
Занимаемое пространство и эффективность использования памяти
Другое важное применение Memory Analyzer — отыскание компонентов, которые используют большую часть кучи, даже в ситуациях без утечки памяти. Если потребление памяти удастся сократить, можно улучшить такие показатели системы, как емкость или производительность. Это позволит поддерживать больше сессий или тратить меньше времени на сборку мусора.
Первый шаг — генерация отчета Top Components. Он разделяет потребление памяти в системе по компонентам, анализирует использование памяти в каждом компоненте и отыскивает наиболее расточительные. Про объект, над которым доминирует другой объект, можно сказать, что он принадлежит этому доминирующему объекту. В отчете Top Components перечисляются все объекты, не принадлежащие никакому другому объекту. Это и есть главные доминирующие объекты кучи. Затем главные доминирующие объекты с помощью классов объектов делятся по загрузчикам классов (classloader), после чего все эти главные доминирующие объекты и принадлежащие им объекты назначаются соответствующим загрузчикам классов. Дальнейший анализ осуществляется посредством выбора одного classloader-загрузчиков в отчете с целью открытия нового отчета по компонентам для этого загрузчика.
Для каждого компонента анализируются объекты Collections. Классы Collections, как показывает java.util.*, предлагают программистам отличные возможности для экономии рабочего времени. Они предоставляют хорошо протестированные реализации для списков, наборов и отображений. Среднее приложение может иметь миллионы коллекций, вследствие чего потери памяти в коллекциях могут быть весьма значительными.
Пустые коллекции –весьма распространенная причина бесполезного расходования памяти. Артефакты ArrayList, Vector, HashMap и HashSet создаются со вспомогательными массивами для хранения записей, размер каждого из которых по умолчанию может составлять, например, 10 записей. Удивительно распространенным явлением в приложениях является создание коллекций, в которых не хранятся никакие объекты. Такой подход может привести к быстрому израсходованию всей доступной памяти. Например, при наличии 100000 пустых коллекций одни только вспомогательные массивы способны потребить 100000 * (24+10*4) байт = 6 МБ.
Отчет Empty Collection (пустые коллекции) рассматривает стандартные классы и расширения коллекций, а затем анализирует их по размерам коллекций. Затем для каждой коллекции он создает таблицу, отсортированную по размеру коллекции (первым включается самый распространенный размер). Если значительная доля экземпляров определенного типа в коллекции является пустой, то отчет отмечает это обстоятельство как возможную потерю памяти.
Одно из решений состоит в том, чтобы отложить выделение коллекция до момента, когда возникнет необходимость вставить в нее какую-либо запись. Другое решение состоит в том, чтобы выделять коллекции с размером по умолчанию 0 или 1, а затем разрешать увеличение этих размеров в случае необходимости за счет ресурсов среды выполнения. Третье решение состоит в сокращении размера коллекции после завершения фазы инициализации.
Родственная область — коллекции с небольшим количеством записей, бесполезно тратящие большой объем памяти. Раздел Collection Fill Ratio для каждого типа коллекций показывает количество экземпляров данной коллекции с конкретным коэффициентом заполнения. Этот механизм выявляет коллекции со значительной долей пустого пространства.
В типичном бизнес-приложении строки и символьные массивы занимают большое пространство памяти, поэтому они представляют собой еще одну область, достойную анализа. Этот раздел отчета о компонентах анализирует строки на предмет наличия общего содержимого. Строки являются неизменными. Спецификация виртуальных машин гарантирует, что строковые константы с одинаковым значением используют один и тот же экземпляр. Динамически создаваемые строки не имеют такой гарантии. Например, два объекта String, созданные посредством чтения данных из базы данных или с диска и имеющих одинаковое значение, будут иметь разные экземпляры и разные символьные массивы. Если эти строки сохраняются, то потребление памяти может оказаться существенным.
Эту проблему можно решить посредством использования String.intern() или поддержания пользовательских структур типа hash set или hash map.
Конструкция String.substring() реализуется на языке Java посредством создания нового объекта String, совместно использующего исходный символьный массив. Это эффективный подход в том случае, если исходная строка по-прежнему необходима. Если необходима лишь небольшая подстрока, то некоторое пространство тратится впустую, поскольку символьный массив сохраняется целиком. Запрос Waste In Char Arrays показывает объем теряемого пространства в символьных массивах, на которые ссылаются только строки.
Пакеты Eclipse и иерархия classloader-загрузчиков
Современные приложения делятся на компоненты (во многих случаях – на основе classloader-загрузчиков) с целью поддержания определенной степени изоляции между фрагментами приложения. Обновление компонентов может осуществляться посредством прекращения использования classloader-загрузчика для определенного компонента и загрузки новой версии этого компонента с помощью нового classloader-загрузчика. В надлежащий момент времени сборщик мусора освобождает память от старой версии, при условии, что приложение не содержит внешних ссылок на классы, на объекты или на classloader-загрузчик из этой версии.
Запрос Class Loader Explorer показывает все classloader-загрузчики в системе и делает это для всех приложений. Этот запрос показывает классы, загруженные classloader-загрузчиком, а также родительскую цепочку этого classloader-загрузчика, что позволяет понять проблемы, возникающие при загрузке классов. Инспектирование позволяет увидеть, существует ли несколько копий некоторого classloader-загрузчика. Если для классов, не имеющих экземпляров, задан classloader-загрузчик, то вероятнее всего этот classloader-загрузчик неактивен.
Запрос на наличие дублирующихся классов показывает имена классов, которые были загружены несколькими classloader-загрузчиками. Это может быть признаком утечки памяти, порожденной classloader-загрузчиком. Чтобы в classloader-загрузчике произошла утечка памяти, достаточно всего одной ссылки на объект, находящийся где-то в другом месте системы, например, в реестре. Этот объект содержит ссылку на свой класс, класс содержит ссылку на classloader-загрузчик, а classloader-загрузчик содержит ссылки на все определенные классы.
OSGi — это общепринятая спецификация для инфраструктуры загрузки классов. Одна из реализаций этой спецификации под названием Eclipse Equinox применяется в основанных на Eclipse приложений для разделения подключаемых модулей, а также в продукте WebSphere® Application Server начиная с версии 6.1. При попытке понять состояние некоторого приложения весьма полезно знать состояние всех пакетов. Обратимся к рисунку 5, на котором показан запрос Eclipse Equinox Bundle Explorer.
Рис. 5. Eclipse Bundle Explorer
Системный дамп или HPROF-дамп содержит все объекты и все поля. Представление Bundle Explorer показывает все пакеты в системе, а также их состояния вместе с их зависимостями, зависимыми объектами и сервисами. Оно может показать разработчику пакеты, которые неожиданно для него являются активными и как таковые потребляют больше ресурсов.
Как показано в Таблице 1, дамп может содержать сведения о потоках, которые дают уникальную возможность понять, что происходило в момент создания дампа. Например, дамп может содержать все активные стеки потоков, все фреймы для каждого потока и, что самое важное, некоторые или все активные локальные переменные Java в этих фреймах.
Представление Thread Overview (см. рис. 6) показывает все потоки в JVM-машине, а также различные атрибуты потоков, такие как размер удерживаемой кучи, контекст classloader, приоритет, состояние и собственный идентификатор ID.
Рис. 6. Представление Thread Overview
Знание размера удерживаемой кучи особенно полезно в случаях, когда при возникновении ошибки OutOfMemoryError проблемы с кучей Java по существу отсутствуют, но сумма удерживаемых потоков становится «слишком большой». В данном случае JVM-машина может иметь недостаточные размеры, размеры пула потока могут быть слишком большими или средняя/максимальная «загрузка» кучи Java для потока может быть слишком высока.
Представление Thread Stacks (рис. 7) показывает каждый поток, его стек, фреймы этого стека и локальные переменные Java в этих фреймах стека.
Рис. 7. Представление Thread Stacks
В примере на рисунке 7, развернут поток типа java.lang.Thread с именем main — это главный поток в простой программе на основе командной строки. Показан каждый фрейм стека данного потока, при этом фреймы с доступными локальными переменными Java могут быть развернуты. В данном случае String передается как аргумент из Play.method1 в Play.method2, а содержимое этой строки, user1, обведено красным кругом. Представьте себе, какие мощные возможности для реконструкции или реверсивного проектирования того, что происходило в момент совершения дампа, открывают знания о том, что происходило в каждом фрейме стека потока и в каких объектах это все происходило..
Обратите внимание, что вследствие оптимизации среды исполнения будут доступны не все связанные объекты, такие как параметры методов или экземпляры объектов (хотя эти объекты будут присутствовать в дампе), однако те объекты, с которыми «активно работают», обычно оказываются доступными.
Если в приложении генерируются исключения, анализ их причин может быть затруднен вследствие дополнительных сложностей. Приведем два примера таких ситуаций.
- Использование механизмов регистрации информации в журнале приводит к потере исключения или к удалению сообщения об исключении.
- Исключение генерирует сообщение, которое содержит недостаточный объем информации.
В первом случае сообщение об исключении или само исключение полностью теряется, что затрудняет осознание существования проблемы или получение базовой информации о ней. Во втором случае исключение регистрируется в журнале, а сообщение об этом исключении и результаты трассировки стека доступны, но не содержат необходимой информации для устранения причины этого исключения.
Инструмент Memory Analyzer имеет доступ к полям внутри объектов, что позволяет отыскать сообщение об исключении, начиная с объекта исключения. Кроме того, в некоторых случаях можно извлечь дополнительные данные, которые отсутствовали в исходном исключении.
Определение местонахождения исключений в моментальном дампе
Один из способов нахождения исключений, присутствующих в моментальном дампе, состоит в использовании возможностей языка OQL в инструменте Memory Analyzer для отыскания в дампе интересующих нас объектов. Например, данный запрос находит все объекты исключений:
SELECT * FROM INSTANCEOF java.lang.Exception exceptions |
Следующий запрос формирует список всех исключений, который можно открыть в представлении Inspector для рассмотрения полей внутри каждого исключения. Зная, что сообщение об исключении содержится в поле detailMessage, можно изменить этот запрос для непосредственного извлечения сообщений об исключениях и немедленного отображения их в рамках таблицы результатов:
SELECT exceptions.@displayName, exceptions.detailMessage.toString() FROM INSTANCEOF java.lang.Exception exceptions |
Результаты предыдущего запроса показаны на рис. 8.
Рис. 8. Результаты OQL-запроса по исключениям, содержащие сообщения об исключениях
На рис. 8 показаны все исключения, которые на данный момент присутствуют в приложении, а также все сообщения, которые отображаются при выдаче соответствующих исключений.
Извлечение дополнительной информации, касающейся исключений
Отыскание в дампе объекта исключения позволяет восстановить сообщение об этом исключении, однако иногда сообщение об исключении имеет слишком общий или слишком неопределенный характер, что не способствует пониманию причин проблемы. В качестве примера рассмотрим исключение java.net. ConnectException. При попытке создать сокетное соединение с хостом, который не принимает соединений, мы получили следующее сообщение:
java.net.ConnectException: Connection refused: connect
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:352)
at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:214)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:201)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:377)
at java.net.Socket.connect(Socket.java:530)
at java.net.Socket.connect(Socket.java:480)
at java.net.Socket.(Socket.java:377)
at java.net.Socket.(Socket.java:220) |
Этого сообщения достаточно, если у нас есть доступ к программному коду, создающему сокет. Этот код позволяет увидеть, какое имя хоста и какой порт используются. В случае более сложного кода, в котором значения «имя хоста/порт» подвержены изменениям, поскольку поступают из внешних источников (вводимые пользователем значения, базы данных и т.п.), это сообщение не поможет нам понять, почему искомое соединение было отвергнуто.
Трассировка стека должна включать объект сокета, содержащий полезные данные. Если мы сможем с помощью Memory Analyzer найти этот объект сокета в моментальном дампе, то сможем узнать имя хоста/порт системы, отказавшиеся от соединения.
Самый простой способ решения этой задачи — генерировать дамп, когда соответствующее исключение будет выдано. В средах исполнения IBM это можно сделать с помощью следующего набора опций -Xdump:
-Xdump:system:events=throw,range=1..1,
filter=java/net/ConnectException#java/net/PlainSocketImpl.socketConnect
|
Эта опция генерирует системный дамп IBM при первом возникновении исключения ConnectException, сгенерированного методом PlainSocketImpl.socketConnect().
После загрузки сгенерированного моментального дампа в Memory Analyzer мы сможем с помощью опции > Java Basics > Thread Stacks сформировать список потоков и объектов, связанных с каждым методом в трассировке стека потоков.
Развернув текущий поток и фреймы методов в этом потоке, мы сможем увидеть объекты, ассоциированные с этими методами. В случае исключения java.net.ConnectException самым интересным является метод java.net.Socket.connect(). Развернув фрейм этого метода, мы увидим ссылку на объект java.net.Socket в памяти. Это и есть то соединение через сокет, которое мы пытались установить.
После выбора объекта Socket соответствующие поля отображаются в представлении Inspector, как показано на рис. 9.
Рис. 9. Представление Inspector для объекта
Socket
Информация на рис. 9 не слишком полезна, поскольку фактическая реализация Socket находится в поле impl. Чтобы проинспектировать содержимое объекта impl, можно или развернуть объект Socket и выбрать строку impl java.net.SocksSocketImpl в главной панели, или в представлении Inspector нажать правой кнопкой на поле impl и выбрать опцию Go Into. Теперь поля для SocksSocketImpl видны в представлении Inspector, как показано на рис. 10.
Рис. 10. Представление Inspector для объекта
SocksSocketImpl
Представление на рис. 10 обеспечивает доступ к полям address и port. В данном случае поле port имеет значение 100, однако поле address указывает на объект java.net.Inet4Address. После выполнения аналогичной процедуры для рассмотрения полей объектов Inet4Address мы получим результаты, показанные на рис. 11:
Рис. 11. Представление Inspector для объекта
Inet4Address
Несложно увидеть, что поле hostName имеет значение baileyt60p.
Перечислим некоторые подсказки и советы, которые могут оказаться полезными.
- Не забывайте, что сам инструмент Memory Analyzer может исчерпать память. В случае использования Eclipse MAT отредактируйте значение
-Xmxв файле MemoryAnalyzer.ini. В случае использования версии ISA отредактируйте файл ISA Install/rcp/eclipse/plugins/com.ibm.rcp.j2se.../jvm.properties. - Если вам по-прежнему не хватает памяти для 32-разрядной версии инструмента Memory Analyzer, используйте 64-разрядную версию Eclipse MAT или попробуйте headless-режим (см. Ресурсы). (Инструмент ISA в настоящий момент не поддерживает 64-разрядный режим).
- Memory Analyzer записывает файлы подкачки в каталог дампа, что уменьшает время перезагрузки дампа. Эти файлы могут быть заархивированы, отосланы на другую машину и помещены в тот же каталог, где находится сам дамп, что избавляет от необходимости полной перезагрузки дампа.
- Если размеры дампа расходятся с данными сборщика мусора на момент осуществления дампа, обратитесь по ссылке Unreachable Objects Histogram на вкладке Overview. Возможно, куча Java содержала много мусора (например, если какая-либо «штатная» коллекция не исполнялась на протяжении некоторого промежутка времени), который был удален инструментом Memory Analyzer.
- Если два объекта A и B не имеют прямых ссылок друг на друга, но у обоих из них есть исходящие ссылки на некоторый набор объектов C, то удерживаемая набором C куча будет включена не в наборы A или B, а в набор объекта, доминирующего над объектами A и B. В некоторых ситуациях B может временно наблюдать за набором C, который фактически является потомком A. В этом случае можно щелкнуть правой кнопкой на A и выбратьJava Basics > Customized Retained Set, после чего использовать адрес B как параметр исключения
(-x). - Можно одновременно загрузить несколько дампов и сравнить их. Откройте опцию Histogram для более «свежего» дампа, нажмите на кнопку Compare в верхней части окна и выберите базовый дамп.
- При рассмотрении дерева ссылок помните, что ссылка способна обращаться, прямо или косвенно, обратно к «родительской» ссылке, в результате чего ваше исследование может войти в замкнутый цикл (например, в связанном списке). Не забывайте об объектных адресах. Кроме того, помните, что если перед именем класса объекта стоит слово
class, значит, вы исследуете статический экземпляр этого класса. - В большинстве представлений отображаемое значение
Stringограничено 1024 символами. Если вам необходима вся строка, нажмите правой кнопкой на объекте и выберите Copy > Save value to file. - У большинства представлений есть опция экспорта, а большая часть HTML-результатов создается в файловой системе. Это позволяет экспортировать данные для совместного использования или для дальнейшего преобразования. Кроме того, можно нажать сочетание клавиш Ctrl+C на любых, предварительно выделенных строках, чтобы скопировать текстовое представление этих строк в буфер обмена.
Согласно описанию на ресурсе Eclipse.org, инструмент Memory Analyzer был первоначально разработан как «быстрый и функционально насыщенный анализатор кучи Java, который помогал бы находить утечки памяти и уменьшать потребление памяти». Однако возможности данного инструмента несомненно простираются далеко за пределы этого описания. В дополнение к своей роли при диагностировании «нормальных» проблем с памятью мгновенные дампы могут быть использованы в качестве альтернативы или в качестве дополнения для других методик выявления проблем, таких как трассировка и «наложение заплат». В частности, при использовании HPROF-дампов и системных дампов от IBM инструмент Memory Analyzer предоставляет содержимое памяти, включая примитивы и имена полей из первоначального исходного кода. С помощью различных представлений, рассмотренных в данной статье, можно исследовать или реконструировать имеющуюся проблему, включая такие аспекты, как общее занимаемое пространство и эффективность использования памяти, пакеты Eclipse и отношения classloader-загрузчиков, использование данных потока и локальные переменные фрейма стека, исключения и т.д. Кроме того, язык OQL и модель подключаемых модулей Memory Analyzer упрощают исследование дампа с помощью языка запросов и программируемых методов, что помогает автоматизировать типовые задачи анализа.
Научиться
- Оригинал статьи
Debugging from dumps:
Diagnose more than memory leaks with Memory Analyzer.(EN)
-
Web-трансляция: Отладка Java-приложений с помощью Memory Analyzer и IBM Extensions for Memory Analyzer. Сессия под руководством Криса Бэйли (Chris Bailey) и Кевина Григоренко (Kevin Grigorenko), посвященная генерации дампов и их использованию для исследования приложений: отыскание утечек памяти, исследование потоков и рассмотрение структур продуктов IBM с помощью IBM Extensions for Memory Analyzer.
-
Блог по Memory Analyzer и Wiki-ресурс по Memory Analyzer: ознакомьтесь с этими ресурсами на сайте Eclipse.org.
-
Использование опции -Xdump: в этом разделе руководства IBM Java Diagnostics Guide 6 содержится информация по опциям
-Xdumpкомандной строки. -
Опции Java VM "HotSpot": Описание синтаксиса для изменения местоположения файла дампа, сгенерированного с помощью JVM-машины HotSpot.
- Получение системных дампов для разных операционных систем.
- AIX: Полное задействование файлов ядра AIX.
- Linux: Настройка и проверка Linux-среды.
- z/OS: Получение SVC-дампов и Настройка дампов.
- Solaris: Руководство по отысканию неисправностей Java SE 6 с помощью HotSpotVM.
-
jmap: Официальная документация поjmap. - Использование дамп-агентов: Сведения по конфигурированию механизма дампов от IBM для генерации системных дампов и дампов кучи.
-
Использование системных дампов и просмотрщика дампов. Описание двух инструментов IBM:
jextractиjdmpview. -
Автоматический анализ дампов кучи: быстрое отыскание утечек памяти: Автоматическое обнаружение утечек памяти с помощью Memory Analyzer.
- Исполнение 64-разрядной виртуальной машины при нативном 32-разрядном SWT: Рекомендации по исполнению инструмента Eclipse Memory Analyzer Tool в 64-разрядном headless-режиме.
- IBM Extensions for
Memory Analyzer: Проект IBM alphaWorks по разработке расширений для Memory Analyzer, упрощающих анализ состояния программных продуктов IBM, включая WebSphere Application Server.
Получить продукты и технологии
-
Инструменты мониторинга и диагностики IBM для Java — Memory Analyzer. : Получите Memory Analyzer в составе IBM Support Assistant.
-
Eclipse Memory Analyzer Tool и IBM DTFJ Plug-in: Загрузите инструмент Eclipse MAT и установите подключаемый модуль DTFJ для использования MAT с дампами IBM PHD и с системными дампами.
- Инструмент
IBM Monitoring and Diagnostics Tools for Java - Health Center: Инструмент Health Center можно использовать для запроса дампа PHD или системного дампа из исполняющегося Java-процесса.
Крис Бейли (Chris Bailey) начал работать в Центре Технологий Java в IBM после окончания Университета в Саутгемптоне в 2000 году. Он много работает с пользователями над решением проблем, возникающих при работе с технологиями Java, выпущенными IBM, и продуктами, работающими на платформе Java. Крис является координатором форума разработчиков, под названием "Среда выполнения IBM Java и SDK" и в данный момент сосредоточил внимание на улучшении качества информации и инструментальных средств, доступных пользователям платформы Java от IBM.

Эндрю Джонсон (Andrew Johnson) — дипломированный инженер, инженер-консультант по программному обеспечению в центре IBM Java Technology Center в Херсли (Великобритания). Он работает в IBM с 1988 года, после получения степени бакалавра по электронной технике в Кембриджском университете (Великобритания). С 1996 года он занимается такими областями, как виртуальные машины Java, JIT-компиляторы и инструменты для диагностирования проблем в Java-решениях. Автор адаптера к продукту Eclipse Memory Analyzer, который читает дампы из виртуальных машин IBM. В настоящее время является одним из основных участников данного проекта.

Кевин Григоренко (Kevin Grigorenko) — инженером в специальной группе по продукту WebSphere Application Server, которая обеспечивает непосредственную и дистанционную дополнительную поддержку во всем мире по дефектам продуктов, особенно в критических для клиентов ситуациях. В настоящее время он занимается преимущественно выявлением проблем в продукте WebSphere Application Server и в стеке связанных продуктов, включая JVM и разные операционные системы. Кроме того, он имеет многолетний опыт разработки на различных платформах, в том числе Java Enterprise Edition, C, C++, Perl, PHP, Python, Ruby и .NET.