Как анализировать дамп памяти java

Обновлено: 04.07.2024

Как анализировать Thread Dump (сборник) - проблемы, которые может решить Thread Dump

jstack pid можно набрать информацию о дампе потока

Когда веб-программа Java замедляется или дает сбой, необходимо использовать дамп потоков. Анализируйте потоки в Java, как создаются потоки, как управлять потоками, как выгружать потоки из запущенных программ и, наконец, как их анализировать, чтобы блокировать и создавать узкие места. к Веб-сервер использует от десятков до сотен потоков для обработки большого количества одновременных пользователей. Если несколько потоков используют общие ресурсы, невозможно избежать конкуренции данных между потоками, и иногда возникают взаимоблокировки. Конкуренция потоков заключается в том, что разные потоки в веб-программе обращаются к общим ресурсам, и один поток ждет, пока другой поток снимет блокировку. Например, при записи журнала, когда поток записывает журнал, он должен сначала получить блокировку, а затем получить доступ к общему ресурсу. Тупик - это особый вид конкуренции потоков. Два или более потока должны ждать, пока другие потоки завершат свои задачи, чтобы завершить свои задачи. Конкуренция потоков принесет множество различных проблем, чтобы проанализировать эти проблемы, вам необходимо использовать Thread Dump. Дамп потока записывает истинное состояние каждого потока.

Стек потоков - это моментальный снимок состояния потоков (включая блокировки) в виртуальной машине в данный момент, то есть состояние выполнения всех потоков в системе в определенный момент, включая стек вызовов каждого потока и состояние удержания блокировки.

Информация о стеке потока содержит :

1. Имя потока, идентификатор, количество потоков и т. Д.

2. Состояние выполнения потока, состояние блокировки (какой поток удерживает блокировку, какой поток ожидает блокировки и т. Д.)

3. Стек вызовов содержит полное имя класса, выполняемый метод, количество строк исходного кода и т. Д.

Стек печати на разных виртуальных машинах немного отличается.

Стек потоков может анализировать следующие проблемы производительности:

1. Системный процессор слишком высок.
2. Узкие места производительности: например, длительное время отклика, но низкие ресурсы ЦП.
3. Система работает все медленнее и медленнее, а время отклика увеличивается.
4. Система зависает, долго нет ответа или долгое время ответа
5. Тупик потока, мертвая петля и т. Д.
6. Переполнение памяти из-за слишком большого количества потоков (например, невозможность создания потоков и т. Д.)

Одновременно может выполняться несколько потоков.Чтобы обеспечить универсальность использования нескольких потоков совместно используемых ресурсов, используется синхронизация потоков, чтобы гарантировать, что только один поток может получить доступ к общим ресурсам одновременно. Для синхронизации потоков можно использовать мониторы в Java. У каждого объекта Java есть монитор, и этот монитор может принадлежать только одному потоку. Когда поток хочет получить монитор, принадлежащий другому потоку, он должен войти в очередь ожидания, пока поток не освободит монитор. Чтобы проанализировать дамп потока, вам необходимо сначала понять состояние потока. Состояние потока находится в java.lang.Thread.State.

Новое состояние (New) Новый объект потока создан.

Состояние готовности (Runnable) После создания объекта потока другие потоки вызывают метод start () объекта. Поток в этом состоянии находится в пуле выполняемых потоков, становится работоспособным и ожидает права на использование ЦП.

Состояние выполнения (Running) Поток в состоянии готовности захватывает ЦП и выполняет программный код.

Заблокированное состояние (Заблокировано) Заблокированное состояние означает, что поток по какой-то причине отказывается от прав на использование ЦП и временно прекращает работу. Пока поток не перейдет в состояние готовности, у него есть шанс перейти в состояние выполнения. Есть три типа блокировки:

  • Ожидание блокировки: запущенный поток выполняет метод wait (), а JVM помещает поток в пул ожидания.
  • Синхронная блокировка: когда запущенный поток получает блокировку синхронизации объекта, если блокировка синхронизации занята другими потоками, JVM помещает поток в пул блокировок.
  • Другая блокировка: когда запущенный поток выполняет метод sleep () или join (), или когда выдается запрос ввода-вывода, JVM переводит поток в заблокированное состояние. Когда время ожидания состояния sleep () истекает, join () ожидает завершения потока или истечения времени ожидания или завершения обработки ввода-вывода, поток возвращается в состояние готовности.

Мертвое состояние (Dead): поток завершает свое выполнение или выходит из метода run () из-за исключения, и поток завершает свой жизненный цикл.

Состояние потока, описываемое ключевыми словами, используемыми Jstack, не совпадает с состоянием потока выше, поэтому может возникнуть путаница в понимании. Хотя потоки в Java имеют те же пять состояний, описанных выше, в реальных ситуациях новое состояние потока и состояние смерти длятся очень недолго, и нас это не особо заботит. В большинстве случаев мы обращаем внимание на состояние работы / состояние блокировки,

Картинка ниже очень важна


  • Когда новый поток (Runnabler) выполняется, вновь созданный поток находится в новом состоянии, которое невозможно выполнить.
  • Когда выполняется thread.start (), поток находится в состоянии выполнения. В этом случае, пока он получает процессор, он может начать выполнение. Поток в состоянии выполнения примет планирование JVM и войдет в состояние выполнения, но когда он войдет в это состояние, случайным образом неизвестно.
  • Поток в состоянии выполнения является наиболее сложным и может входить в состояния выполнения, ожидания, timed_waiting, заблокирован и мертв:
  • Если ЦП запланирован для другого потока или выполняется метод Thread.yield (), он переходит в рабочее состояние, но также может немедленно перейти в рабочее состояние.
  • Если выполняется Thread.sleep (long) или thread.join (long), или метод object.wait (long) вызывается для объекта блокировки, он переходит в состояние timed_waiting
  • Если выполняется thread.join () или вызывается метод object.wait () для объекта блокировки, он переходит в состояние ожидания
  • Если вы введете синхронизированный метод или синхронизированный кодовый блок без получения объекта блокировки, он перейдет в заблокированное состояние.
  • Если поток в состоянии ожидания входит в ожидание из-за метода thread.join (), после того, как целевой поток будет выполнен, он вернется в состояние выполнения; если это происходит из-за того, что метод object.wait () входит в ожидание, он выполняется для объекта блокировки object.notify () или object.notifyAll () вернется в рабочее состояние
  • Поток в состоянии timed_waiting аналогичен состоянию ожидания, за исключением того, что по истечении установленного времени он вернется в рабочее состояние.
  • Поток в заблокированном состоянии выйдет из заблокированного состояния только после получения блокировки.
  • Когда поток выполняется или возникает неперехваченное исключение, он переходит в мертвое состояние, и поток завершается.

В коде 6 состояний, кроме RUNNING. Чтобы указать, что поток выполняется, добавляется RUNNING.

Нам нужно сосредоточиться на четырех состояниях RUNNABLE, BLOCKED, WAITING и TIME_WAITING, которые также появятся в стеке потоков, напечатанном jstack.Пример кода

1) ЗАБЛОКИРОВАНО: поток ожидает блокировки синхронизированного блока или синхронизированного метода. Два заблокированных потока заблокированы.

2) WAITING: это лучше, чем состояние BLOCKED, что означает, что блокировка была получена, но поскольку некоторые условия не выполняются, вы должны подождать некоторое время и вызвать метод object.wait (). Когда условия выполнены, другие потоки вызывают notify, а затем звонят мне. Кроме того, вы также можете вызвать метод Thread.join (). Как следует из названия, он должен вызвать метод соединения других потоков и позволить другим присоединиться и выполнить его первыми. Тогда я могу только ждать. Но поскольку wait () и notify () и notifyAll () используются для координации доступа к общим ресурсам, они должны использоваться в синхронизированном блоке. Таким образом, даже если поток в состоянии ожидания пробуждается notfiy, ему необходимо снова получить блокировку, поэтому он переходит в состояние «Заблокировано» после пробуждения.

3) TIMED_WAITING: аналогично WAITING, разница в том, что нет необходимости просыпаться с помощью метода notify () или notifyAlL (), и вы просыпаетесь, когда время истекло. Кроме того, легче понять сон, то есть позволить текущему потоку спать некоторое время, разница с ожиданием составляет Не снимает блокировку 。

4) ВЫПОЛНЯЕМЫЙ Само собой разумеется, что он уже запущен на виртуальной машине JAVA, но ожидает ресурсов операционной системы, таких как срезы времени ЦП.

Поток создан, но не был выполнен (он был новым и не вызывал метод start ()). Очень мало в файле дампа потока.

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

Поток в состоянии RUNNABLE определенно потребляет процессор? Не обязательно, как операция ввода-вывода сокета, поток читает данные из сети, хотя состояние потока RUNNABLE, на самом деле сеть io, поток приостановлен большую часть времени Да, поток будет пробужден только при поступлении данных. Приостановка происходит в собственном коде (native). Виртуальная машина совсем не согласована. В отличие от явного вызова методов сна и ожидания виртуальная машина может знать истинное состояние потока. Но при зависании в собственном коде виртуальная машина не может знать реальный статус потока, поэтому он всегда отображается как RUNNABLE.

Заблокированным состоянием является то, что поток по какой-то причине отказывается от права на использование ЦП и временно прекращает работу. Чтобы получить монитор, поток должен ждать, пока другие потоки освободят блокировку. Заблокирован (на мониторе объекта), поток находится в заблокированном состоянии, ожидая блокировки монитора. Обычно это происходит потому, что этот поток разделяет блокировку с другими потоками. Другие потоки используют эту блокировку для входа в блок или метод метода синхронизированной синхронизации, и этому потоку также нужна эта блокировка для входа в этот блок синхронизированного кода, что в конечном итоге приводит к тому, что поток находится в заблокированном состоянии. Например: автомобиль может использоваться несколькими людьми в разные периоды времени, но только один человек может пользоваться им одновременно. После того, как пользователь A уехал, пользователь B был ЗАБЛОКИРОВАН, если он не мог его использовать. Автомобиль можно понимать как блокировку, пользователя A можно понимать как поток A, а пользователя B можно понимать как поток B. Поскольку пользователь A уже получил блокировку (автомобиль), а поток B заблокирован на блокировке (автомобиль), он должен дождаться, пока поток A снимет блокировку, прежде чем поток B сможет получить блокировку (автомобиль). Это означает, что после того, как поток владеет определенной блокировкой, он вызывает свой метод ожидания и ждет, пока другие потоки / владельцы блокировки вызовут notify / notifyAll, как только поток сможет продолжить следующий шаг. Здесь нам нужно различать разницу между BLOCKED и WATING: один ожидает входа за пределами критической точки, другой ждет, пока другие не уведомят внутри критической точки, когда поток вызывает метод соединения для присоединения к другому потоку, он также переходит в состояние WAITING, ожидая Выполнение его потока соединения заканчивается, Потоки в состоянии ожидания в основном не потребляют процессор 。

Способы войти в состояние ОЖИДАНИЕ:

wait () переводит поток в состояние блокировки, имеет две формы:

Один позволяет указать в качестве параметра период времени в миллисекундах, другой не имеет параметров. Прежний поток повторно входит, когда вызывается соответствующий notify () или указанное время превышено. Состояние исполняемого файла - состояние готовности , Последний должен быть разбужен соответствующим методом notify () или notifyAll () .После того, как поток будет пробужден, он войдет в пул блокировок и будет ожидать получения метки блокировки. Когда вызывается wait (), он входит в очередь ожидания, и поток освобождает "флаг блокировки", который он удерживает. , Чтобы другие синхронизированные данные в объекте, где расположен поток, могли использоваться другими потоками 。

Wait on condition
_The thread is either sleeping or waiting to be notified by another thread._
Это состояние появляется, когда поток ожидает наступления определенного условия или сна. Конкретную причину можно проанализировать в сочетании со stacktrace. Наиболее распространенная ситуация заключается в том, что поток ожидает чтения и записи сетью. Например, когда сетевые данные не готовы для чтения, поток находится в этом состоянии ожидания. Как только данные будут готовы к чтению, поток повторно активируется, считывает и обрабатывает данные. До того, как Java представила новый ввод-вывод, для каждого сетевого подключения существовал соответствующий поток для обработки сетевых операций чтения и записи.Даже если нет доступных для чтения и записи данных, поток по-прежнему блокируется при операциях чтения и записи, что может привести к неэффективной трате ресурсов. , А также оказать давление на планирование потоков операционной системы. В New IO принят новый механизм, а производительность и масштабируемость написанной серверной программы были улучшены.
Если вы обнаружите, что большое количество потоков из стека потоков находится в состоянии ожидания в состоянии ожидания, они ожидают чтения и записи по сети. Это может быть признаком узкого места в сети. Поток не может быть выполнен из-за перегрузки сети. Одна ситуация заключается в том, что сеть очень загружена, почти вся полоса пропускания потребляется, и все еще существует много данных, ожидающих чтения и записи сетью; другая ситуация также может заключаться в том, что сеть простаивает, но из-за проблем с маршрутизацией пакеты не могут поступать нормально. Следовательно, необходимо объединить некоторые инструменты наблюдения за производительностью системы для проведения всестороннего анализа, например, netstat подсчитывает количество отправленных пакетов в единицу времени, чтобы увидеть, явно ли оно превышает предел пропускной способности сети; наблюдайте за использованием процессора, чтобы увидеть, очевидно ли время ЦП в состоянии системы Больше, чем время процессора в пользовательском режиме; если программа запущена на платформе Solaris 10, вы можете использовать инструмент dtrace, чтобы увидеть ситуацию с системным вызовом, если вы заметили количество системных вызовов чтения / записи или время выполнения намного опережает; все это указывает на пропускную способность сети Узкое место в сети, вызванное ограничением. Другой распространенный случай ожидания ожидания - это то, что поток находится в спящем режиме и будет пробужден, когда время ожидания для сна истечет.

Waiting for Monitor Entry and in Object.wait()

_The thread is waiting to getthe lock for an object (some other thread may be holding the lock). Thishappens if two or more threads try to execute synchronized code. Note that thelock is always for an object and not for individual methods._

В многопоточной программе JAVA для достижения синхронизации между потоками мы должны говорить о Monitor. Монитор - это основной метод, используемый для достижения взаимного исключения и взаимодействия между потоками в Java. Его можно рассматривать как блокировку объекта или класса. У каждого объекта есть только один монитор. Каждый монитор может принадлежать только одному потоку в определенное время. Этот поток называется «ActiveThread», а другие потоки - «ожидающий поток», ожидающие в двух очередях «Entry Set» и «Wait Set» соответственно. Состояние потока, ожидающего в «Наборе записей», - «Ожидание входа в систему», а состояние потока, ожидающего в «Наборе ожидания», - «в Object.wait ()».
Сначала посмотрите на потоки в «Entry Set». Мы называем сегмент кода, защищенный синхронизированным, критическим разделом. Когда поток обращается к критическому разделу, он попадает в очередь «Entry Set». Соответствующий код выглядит так:
synchronized(obj) .
>
На данный момент есть две возможности:
Монитор не принадлежит другим потокам, и в наборе ввода нет других ожидающих потоков. Этот поток становится владельцем монитора соответствующего класса или объекта и выполняет код критической секции.
Монитор принадлежит другим потокам, и этот поток ожидает в очереди набора записей. к
В первом случае поток будет в состоянии «Runnable», а во втором случае поток DUMP будет отображаться как «ожидающий записи монитора».

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

Снова посмотрите на потоки в "Подождите". Когда поток получает Monitor и входит в критическую секцию, если он обнаруживает, что условия для продолжения работы потока не выполняются, он вызывает метод wait () объекта (обычно синхронизированного объекта), закрывает Monitor и входит в очередь «Wait Set». Только когда другие потоки вызывают notify () или notifyAll () для объекта, потоки в очереди «Wait Set» получают возможность состязаться, но только один поток получает монитор объекта и возвращается в рабочее состояние. Поток в «Ожидании» представлен в DUMP как: в Object.wait ().

Обычно, когда ЦП очень занят, сосредоточьтесь на потоке исполняемого, а когда ЦП простаивает, сосредоточьтесь на потоке ожидания записи монитора.

В программе курса Разработчик Java довольно много тем, посвященных внутренностям работы JVM. Мы разбираемся в механизмах работы коллекций, байт-кода, сборщика мусора и т.д. Сегодня предлагаем Вашему внимаю перевод довольно интересной статьи о thread dump-е. Что это такое, как его получить и как использовать.

Хотите узнать, как анализировать thread dump (дамп потоков)? Заходите под кат, чтобы узнать больше о том как в Java получить thread dump и что с ним потом делать.

Большинство современных Java-приложений являются многопоточными. Многопоточность может существенно расширить функционал приложения, в то же время она вносит существенную сложность.

В однопоточном приложении все ресурсы (разделяемая память, операции ввода/вывода и т.д.) могут использоваться без синхронизации, т.к. в любой момент времени только один поток пользуется ресурсом.

В случае многопоточных приложений необходимо найти компромисс между усложнением программы и возможным повышением производительности, когда несколько потоков могут использовать все доступные (часто больше одного) ядра центрального процессора (CPU). Если сделать все правильно, то используя многопоточность (формализована в Amdahl's Law), можно добиться существенного прироста производительности приложения. Однако при этом надо помнить об обеспечении синхронного доступа нескольких потоков к разделяемому ресурсу. В большинстве случаев, фреймворки, такие как Spring, инкапсулируют работу с потоками и скрывают от пользователей многие технические детали. Однако и в случае применения современных сложных фреймворков что-то может пойти не так, и мы, как пользователи, столкнемся со сложно решаемыми багами многопоточности.

К счастью, Java оснащена специальным механизмом для получения информации о текущем состоянии всех потоков в любой момент времени — это thread dump (своего рода моментальный снимок). В этой статье мы изучим, как получить thread dump для приложения реалистичных размеров и как этот dump проанализировать.

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

Основная терминология

С первого взгляда Java thread dump-ы могут показаться «китайской грамотой», ключом к ее понимаю являются следующие понятия. В общем, давайте, повторим основные термины многопоточности, которые будем использовать для анализа дампов.

    Thread или поток — дискретная единица многопоточности, управляемая Java Virtual Machine (JVM). Потоки JVM соответствуют потокам в операционной системе (OS) — native threads («естественные потоки»), которые и реализуют механизм выполнения кода.

У каждого потока есть уникальный идентификатор и имя. Потоки могут быть «демонами» и «не демонами».

Программа завершает свою работу, когда завершаются все потоки «не демоны» или вызывается метод Runtime.exit. Работающие «демоны» не влияют на завершение работы программы. Т.е. JVM ждем когда доработают все «не демоны» и завершает работу, на «не демонов» не обращает внимание.

За более подробной информацией обращайтесь к документации класса Thread.
Поток может находится в одном из следующих состояний:

  • Alive thread или «живой» — поток, который выполняет некоторую работу (нормальное состояние).
  • Blocked thread или «заблокированный» — поток, который попытался зайти в секцию синхронизации (synchronized), однако другой поток уже успел зайти в этот блок первым, и все следующие потоки, которые попытаются зайти в этот же блок оказываются заблокированными.
  • Waiting thread или «ожидающий» — поток, который вызвал метод wait (возможно, с указанием таймаута) и сейчас ждет, когда другой метод выполнит notify или nonifyAll на этом же объекте.

Каждый объект в Java имеет монитор, при помощи которого поток может синхронизоваться, т.е. выставить блокировку, которая гарантирует, что ни один другой поток не получит доступ к этому объекту, пока блокировка не будет снята, т.е. поток — владелец блокировки не выйдет из блока synchronized.

Более детальную информацию можно найти в этих источниках:

Применяя эти простые понятия о потока в Java, мы можем создать тестовое приложение. Для этого приложения мы соберем thread dump. Полученный дамп проанализируем и извлечем полезную информацию о текущих потоках приложения.

Создание примера программы

Прежде, чем создать thread dump, нам надо разработать Java-приложение. Традиционный «hello, world!» для нашей цели слишком прост, а дамп среднего размера приложения может оказаться слишком сложным для демонстрации. Исходя из этого, мы создадим достаточно простое приложение, в котором создаются два потока. Причем потоки попадают в deadlock:


Эта программа создает два ресурса: resourceA и resourceB, и стартует два потока: threadLockingResourceAFirst и threadLockingResourceBFirst, которые блокируют ресурсы друг друга.

Причиной возникновения deadlock-а является «перекрестная» блокировка ресурсов потоками.

Причиной возникновения deadlock является попытка «взаимного» захвата ресурсов, т.е. поток threadLockingResourceAFirst захватывает ресурс resourceA, поток threadLockingResourceBFirst захватывает ресурс resourceB. После этого поток threadLockingResourceAFirst, не отпуская свой ресурс, пытается захватить resourceB, а поток threadLockingResourceBFirst, не отпуская свой ресурс, пытается захватить ресурс resourceA. В результате потоки блокируются. Задержка в 1с добавлена, чтобы гарантировать возникновение блокировки. Потоки ждут освобождение нужных ресурсов, но это никогда не случится.

Вывод программы будет таким (числа после java.lang.Object@ будут разные для каждого запуска):

Генерация Thread Dump

На практике, Java-программа может аварийно завершиться и при этом создать thread dump. Однако в ряде случаев (например в случае deadlock-ов), программа не завершается и thread dump не создает, она просто зависает. Для создания дампа таких зависших программ, прежде всего надо выяснить идентификатор процесса программы, т.е. Process ID (PID). Для этого можно воспользоваться утилитой JVM Process Status (JPS), которая начиная с версии 7, входит в состав Java Development Kit (JDK). Чтобы найти PID процесса нашей зависшей программы, мы просто выполним jps в терминале (Windows или Linux):


Первая колонка — это идентификатор локальной виртуальной машины (Local VM ID, т.е. lvmid) для выполняемого Java-процесса. В контексте локальной JVM, lvmid указывает на PID Java-процесса.

Надо отметить, что это значение, скорее всего, будет отличаться от значения выше. Вторая колонка — это имя приложения, которое может указывать на имя main-класса, jar-файла или быть равно «Unknown». Все зависит от того, как приложение было запущено.

В нашем случае имя приложения DeadlockProgram — это имя main-классы, который был запущен при старте программы. В примере выше PID программы 11568, этой информации достаточно для генерации thread dump'а. Для генерации дампа мы воспользуемся утилитой jstack, которая входит в состав JDK, начиная с версии 7. Чтобы получить дамп мы передадим в jstack в качестве параметра PID нашей программы и укажем флаг -l (создание длинного листинга). Вывод утилиты перенаправим в текстовый файл, т.е. thread_dump.txt:


Полученный файл thread_dump.txt содержит thread dump нашей зависшей программы и содержит важную информацию для диагностики причин возникновения deadlock-а.

Если используется JDK до 7 версии, то для генерации дампа можно воспользоваться утилитой Linux — kill с флагом -3. Вызов kill -3 отправит программе сигнал SIGQUIT.

В нашем случае вызов будет такой:

Анализ простого Thread Dump

Открыв файл thread_dump.txt, мы увидим примерно следующее содержание:

Introductory Information

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

В первой строке указывается время, когда дамп был сформирован, во второй — диагностическая информация о JVM, на которой дамп был получен:


В этой секции нет информации о потоках. Тут задается общий контекст системы, в которой был собран дамп.

Общие сведенья о потоках

В следующей секции представлена информация о потоках, которые выполнялись в системе в момент сбора дампа:


В следующей секции приводится список:

В нем содержится информация о потоках за пределами JVM, т.е. это не потоки виртуальной машины и не потоки сборщика мусора. Если посмотреть на адреса этих потоков, то можно заметить, что они соответствуют значению tid — «естественному, железному» (native) адресу в операционной системе, а не Thread ID.

Троеточия используются для сокрытия излишней информации:

Потоки

Сразу после блока SMR следует список потоков. Первый поток в нашем списке — Reference Handler:

Краткое описание потока

Состояние потока

Вторая строка — это текущее состояние потока. Возможные состояния потока приведены в enum:
Thread.State:

NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED

Более подробную информацию смотрите в документации.

Thread Stack Trace

Следующая секция содержит stack trace потока в момент снятия дампа. Этот stack trace очень похож на stack trace, который формируется не перехваченным исключением. И содержит имена классов и строк, которые выполнялись в момент формирования дампа. В случае потока Reference Handler мы не видим ничего интересного.

Однако в трассировке потока Thread-02 есть кое-что интересное, отличное от стандартного трейса:


В трассировке мы видим, что добавилась информация о блокировке. Этот поток ожидает блокировку на объекте с адресом 0x00000000894465b0 (тип объекта java.lang.Object). Более того поток сам удерживает блокировку с адресом 0x00000000894465a0 (тоже объект java.lang.Object). Эта информация нам пригодится далее для диагностики deadlock-а.

Захваченные примитивы синхронизации (Ownable Synchronizer)

В последней секции приводится список примитивов синхронизации (synchronizers), захваченных потоком. Это объекты, которые могут быть использованы для синхронизации потоков, например, защелки (locks).

В соответствии с официальной документацией Java, Ownable Synchronizer — это наследники AbstractOwnableSynchronizer (или его подкласса), которые могут быть эксклюзивно захвачены потоком для целей синхронизации.

ReentrantLock и write-lock, но не read-lock класса ReentrantReadWriteLock — два хороших примера таких «ownable synchronizers», предлагаемых платформой.

Для получения более подробной информации по этому вопросу можно обратиться к этому
посту.

Потоки JVM

В следующей секции дампа содержится информация о технических потоках JVM, которые не являются частью приложения и связаны с потоками операционной системы. Т.к. эти потоки работают вне приложения, у них нет идентификаторов потока. Чаще всего это потоки сборщика мусора и другие технические потоки JVM:

Глобальные ссылки JNI

В этой секции указывается количество глобальных ссылок, используемых JVM через JNI. Эти ссылки не обслуживаются сборщиком мусора и в определенных обстоятельствах могут стать причиной утечки памяти.


В большинстве простых случаев эта информация не используется. Однако важность глобальных ссылок надо понимать. Более подробную информацию смотрите в этом посте.

Взаимно заблокированные (Deadlocked) потоки


В первом подразделе описывается сценарий взаимной блокировки (deadlock):

Поток Thread-0 ожидает возможность захватить монитор (это обращение к блоку synchronized(secondResource) в нашем приложении), в то же время этот поток удерживает монитор, который пытается захватить поток Thread-1 (это обращение к тому же фрагменту кода: synchronized(secondResource) в нашем приложении).

Эта циклическая блокировка по другому называется deadlock. На рисунке ниже
эта ситуация представлена в графическом виде:


Во втором подразделе для обоих заблокированных потоков приведен stack trace.

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


Это приложение завершится без взаимной блокировки, и в качестве результата мы получим следующий вывод (обратите внимание на то, что адреса экземпляров класса Object изменились):

Анализ более сложных Thread Dump-ов

Дампы настоящих приложений могут быть очень большими и сложными.

В одной JVM одновременно могут быть запущены сотни тысяч потоков. И заблокированными могут оказаться больше чем два потока (или может быть несколько проблем многопоточности, вызванных одной проблемой).

Анализ такого громадного объема информации может стать настоящей проблемой.

Для анализа больших дампов предназначены специальные утилиты-анализаторы — Thread Dump Analyzers (TDAs). Эти утилиты парсят Java thread dump-ы и выводят информацию в человеко-читаемом виде, часто с применением графических средств. Более того, некоторые из них могут выполнить статический анализ и найти причину проблемы. Конечно, выбор конкретной утилиты зависит от целого ряда обстоятельств.

Тем не менее приведем список наиболее популярных TDA:

Заключение

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

В этой статье мы разработали приложение с deadlock, сформировали дамп зависшего приложения. Проанализировали дамп и нашли причину блокировки и исправили её. Это не всегда так просто, для анализа большинства реальных приложений очень полезно использовать специальные утилиты — анализаторы.

Тем не менее, каждый профессиональный Java-разработчик должен понимать основы анализа thread dump-ов. Должен ориентироваться в их структуре, знать какую информацию можно извлечь и как ее использовать для решения проблем многопоточности.

Хотя, thread dump — это не «серебряная пуля» в мире многопоточности, тем не менее это важное средство диагностирования сложных, но довольно распространенных проблем многопоточных Java-приложений.

В программе курса Разработчик Java вопросы многопоточности занимают заметную часть. Мы детально рассматриваем как разрабатывать программы так, чтобы не приходилось по ночам разбираться с deadlock-в продакшене.

Как всегда интересны ваши мнения и комментарии, которые можно оставить тут или заглянуть к Виталию на день открытых дверей.

Основные симптомы утечек памяти Java

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

Ошибки конфигурации выглядящие как утечки памяти

Перед тем, как заглянете в ситуации вызывающие проблемы с памятью Java и проведете анализ, необходимо убедиться, что исследования не имеют отношения к абсолютно другой задаче. Часть ошибок out-of-memory возникают из-за различных ошибок, например ошибок конфигурации. У приложения, возможно, недостаток памяти в куче или оно конфликтует в системе с другими приложениями. Если начинаете говорить о проблемах нехватки памяти, но не можете определить что вызывает утечку, взгляните на приложение по-другому. Обнаружится, что нужно сделать изменения в потоке финализации или увеличить объем permanent generation пространства, являющегося областью памяти JVM для хранения описания классов Java и некоторых дополнительных данных.

Преимущества инструментов мониторинга памяти

Инструменты мониторинга памяти дают бОльшую видимость использования доступных ресурсов приложением Java. Используя данное ПО, вы делаете шаг для сужения поиска корня проблемы утечки памяти и прочих инцидентов связанных с производительностью. Инструменты идут в нескольких категориях, и вам, возможно, нужно использовать множество приложений, чтобы разобраться как начать правильно обозначать проблему и что пошло не так, даже если вы имеете дело с утечками памяти. Heap dump (дампа кучи) файлы дают необходимые сведения для анализа Java-памяти. В этом случае вам нужно использовать два инструмента: один для генерации дамп-файла и другой для подробного анализа. Такое решение дает детализированную информацию о том, что происходит с приложением. Один раз инструмент указывает места возможных проблем и работает над сужением площади, чтобы обнаружить точное место возникновения инцидента. И этот период времени - время самой длинной и портящей настроение части проб и ошибок. Анализатор памяти указывает несколько проблем в коде, но вы не уверены абсолютно, с какими проблемами столкнулось ваше приложение. Если всё ещё сталкиваетесь с прежней ошибкой, начните сначала и поработайте над другой возможной ошибкой. Сделайте одно изменение за раз и попытайтесь продублировать ошибку. Нужно будет дать приложению поработать некоторое время, чтобы продублировать условия возникновения ошибки. Если при первом тесте происходит утечка памяти, не забудьте протестировать приложение под нагрузкой. Приложение может работать отлично с небольшим количеством данных, но может снова выбросить прежние ошибки при работе с большим объемом данных. Если еще возникает всё та же самая ошибка, нужно начать сначала и разобрать другую возможную причину. Инструменты мониторинга памяти доказывают свою пользу после того, когда приложение стало полностью работающим. Можно удаленно наблюдать за производительностью JVM и проактивным обнаружением сбойных ситуаций перед тем, как разработчик погрузится в проблему и будет собирать исторические данные производительности, чтобы помочь себе в будущем улучшить техники программирования и наблюдать как Java работает под тяжелой нагрузкой. Многие решения включают режимы оповещения "опасность" или другие подобные режимы и разработчик сразу может знать, что происходит не так, как хотелось. Каждый разработчик не хочет, чтобы критическое приложение, будучи в промэксплуатации, падало и являлось причиной потери десятков или сотен тысяч долларов во время простоя приложения, поэтому инструменты мониторинга памяти уменьшают время реагирования разработчика. Приложения мониторинга памяти дают начать процесс диагностики мгновенно, вместо того, чтобы попросить вас пойти к заказчику, где никто не скажет какая именно ошибка случилось или какой код ошибки выдало приложение. Если часто погружаетесь в проблемы памяти и производительности вашего Java-приложения, плотно возьмитесь за процесс тестирования. Обозначьте каждую слабую область в процессе разработки и измените стратегии тестирования. Посоветуйтесь с коллегами и сравните свои подходы тестирования с существующими лучшими практиками. Иногда вам надо пересмотреть маленький фрагмент кода и далее обеспечить длительное воздействие на все приложение.

Роль Garbage Collector на память Java и утечки памяти

Garbage Collector (cборщик мусора) в Java играет ключевую роль в производительности приложения и использования памяти. Он ищет неиспользуемые (мертвые) объекты и удаляет их. Эти объекты больше не занимают память, так что ваше приложение продолжает обеспечивать доступность ресурсов. Иногда приложение не дает GC достаточно времени или ресурсов для удаления мертвых объектов и они накапливаются. Можно столкнуться с такой ситуацией когда идет активное обращение к объектам, которые, вы полагаете, мертвы. Сборщик мусора не может сделать ничего c этим, т.к. его механизм автоматизированного управления памяти обходит активные объекты. Обычно сборщик мусора работает автономно, но необходимо настроить его поведение на реагирование тяжелых проблем с памятью. Однако, GC может сам приводить к проблемам производительности.

Области GC

Сборщик мусора для оптимизации сборки разделяет объекты на разные области. В Young Generation представлены объекты, которые отмирают быстро. Сборщик мусора часто работает в этой области, с того момента, когда он должен проводить очистку. Объекты оставшиеся живыми по достижению определенного периода переходят в Old Generation. В области Old Generation объекты остаются долгое время, и они не удаляются сборщиком так часто. Однако, когда сборщик работает в области, приложение проходит проходит через большую операцию, где сборщик смотрит сквозь живые объекты для очистки мусора. В итоге объекты приложения находятся в конечной области permanent generation. Обычно, эти объекты включают нужные метаданные JVM. Приложение не генерирует много мусора в Permanent Generation, но нуждается в сборщике для удаления классов когда классы больше не нужны.

Связь между Garbage Collector и временем отклика

Сборщик мусора, независимо от приоритета исполнения потоков приложения, останавливает их не дожидаясь завершения. Такое явление называется событием "Stop the World". Область Young Generation сборщика мусора незначительно влияет на производительность, но проблемы заметны, если GC выполняет интенсивную очистку. В конечном итоге вы оказываетесь в ситуации, когда минорная сборка мусора Young Generation постоянно запущена или Old Generation переходит в неконтролируемое состояние. В такой ситуации нужно сбалансировать частоту Young Generation с производительностью, которая требует увеличение размера этой области сборщика. Области Permanent Generation и Old Generation сборщика мусора значительно влияют на производительность приложения и использования памяти. Эта операция major очистки мусора проходит сквозь heap, чтобы вытолкнуть отмершие объекты. Процесс длится дольше чем minor сборка и влияние на производительность может идти дольше. Когда высокая интенсивность очистки и большой размер области Old Generation, производительность всего приложения увязывает из-за событий "Stop the world". Оптимизация сборки мусора требует мониторинга как часто программа запущена, влияния на всю производительность и способов настройки параметров приложения для уменьшения частоты мониторинга. Возможно нужно будет идентифицировать один и тот же объект, размещенный больше, чем один раз, причем приложению не нужно отгораживаться от размещения или вам надо найти точки сжатия, сдерживающие всю систему. Получение правильного баланса требует уделения близкого внимания ко всему от нагрузки на CPU до циклов вашего сборщика мусора, особенно если Young и Old Generation несбалансированы. Адресация утечек памяти и оптимизация сборки мусора помогает увеличить производительность Java-приложения. Вы буквально жонглируете множеством движущихся частей. Но с правильным подходом устранения проблем и инструментами анализа, спроектированных чтобы дать строгую видимость, вы достигнете света в конце туннеля. В противном случае замучаетесь с возникающими неполадками связанных с произодительностью. Тщательное размещение памяти и её мониторинг играют критическую роль в Java-приложении. Необходимо полностью взять в свои руки взаимодействие между сборкой мусора, удалением объектов и производительностью, чтобы оптимизировать приложение и избежать ошибок упирающихся в нехватку памяти. Инструменты мониторинга дают оставаться на высоте, чтобы обнаружить возможные проблемы и обозначить тенденции утилизации памяти так, что вы принимаете проактивный подход к исправлению неисправностей. Утечки памяти часто показывают неэффективность устранения неисправностей обычным путем, особенно если вы сталкиваетесь с неверными значениями параметров конфигурации, но решения вопросов связанных с памятью помогают быстро избежать инцидентов стоящих у вас на пути. Совершенство настройки памяти Java и GC делают ваш процесс разработки намного легче.

В этой статье мы покажем различные способы записи дампа кучи на Java.

Дамп кучи - это снимок всех объектов, находящихся в памяти JVM в определенный момент . Они очень полезны для устранения проблем с утечкой памяти и оптимизации использования памяти в приложениях Java.

Дампы кучи обычно хранятся в файлах hprof двоичного формата. Мы можем открывать и анализировать эти файлы с помощью таких инструментов, как jhat или JVisualVM. Кроме того, для пользователей Eclipse очень часто используется MAT.

В следующих разделах мы рассмотрим несколько инструментов и подходов для создания дампа кучи и покажем основные различия между ними.

2. Инструменты JDK

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

В следующих разделах мы покажем, как использовать эти инструменты для захвата дампов кучи.

2.1. jmap

jmap - это инструмент для вывода статистики о памяти в работающей JVM. Мы можем использовать его для локальных или удаленных процессов.

Чтобы записать дамп кучи с помощью jmap, нам нужно использовать опцию dump :

Наряду с этой опцией мы должны указать несколько параметров:

  • live : если установлено, он печатает только те объекты, которые имеют активные ссылки, и отбрасывает те, которые готовы к сборке мусора. Этот параметр не обязателен
  • format = b : указывает, что файл дампа будет в двоичном формате. Если не установить, результат будет таким же
  • file : файл, в который будет записан дамп
  • pid : идентификатор процесса Java

Пример будет таким:

Помните, что мы можем легко получить pid процесса Java с помощью команды jps .

Имейте в виду, что jmap был представлен в JDK как экспериментальный инструмент и не поддерживается. Поэтому в некоторых случаях может быть предпочтительнее использовать вместо этого другие инструменты.

2.2. jcmd

jcmd - это очень полный инструмент, который работает, отправляя запросы команд в JVM. Мы должны использовать его на той же машине, где запущен процесс Java.

Одна из его многочисленных команд - GC.heap_dump . Мы можем использовать его для получения дампа кучи, просто указав pid процесса и путь к выходному файлу:

Мы можем выполнить его с теми же параметрами, что и раньше:

Как и в случае с jmap, дамп создается в двоичном формате.

2.3. JVisualVM

JVisualVM - это инструмент с графическим пользовательским интерфейсом, который позволяет нам отслеживать, устранять неполадки и профилировать приложения Java . Графический интерфейс простой, но очень интуитивно понятный и легкий в использовании.

Один из его многочисленных вариантов позволяет нам делать дамп кучи. Если мы щелкнем правой кнопкой мыши по процессу Java и выберем опцию «Дамп кучи» , инструмент создаст дамп кучи и откроет его на новой вкладке:


Обратите внимание, что мы можем найти путь к файлу, созданному в разделе «Основная информация» .

Начиная с JDK 9, Visual VM не входит в дистрибутивы Oracle JDK и Open JDK. Поэтому, если мы используем Java 9 или более новые версии, мы можем получить JVisualVM с сайта проекта с открытым исходным кодом Visual VM.

3. Автоматическое создание дампа кучи

Все инструменты, которые мы показали в предыдущих разделах, предназначены для захвата дампов кучи вручную в определенное время. В некоторых случаях мы хотим получить дамп кучи при возникновении ошибки java.lang.OutOfMemoryError, чтобы помочь нам исследовать ошибку.

Для этих случаев Java предоставляет параметр командной строки HeapDumpOnOutOfMemoryError, который генерирует дамп кучи, когда генерируется java.lang.OutOfMemoryError :

По умолчанию он сохраняет дамп в файле java_pid.hprof в каталоге, в котором мы запускаем приложение. Если мы хотим указать другой файл или каталог, мы можем установить его в опции HeapDumpPath :

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

В приведенном выше примере он был записан в файл java_pid12587.hprof .

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

Наконец, этот параметр также можно указать во время выполнения с помощью MBean HotSpotDiagnostic . Для этого мы можем использовать JConsole и установить для параметра виртуальной машины HeapDumpOnOutOfMemoryError значение true :


Мы можем найти больше информации о MBeans и JMX в этой статье.

4. JMX

The last approach that we'll cover in this article is using JMX. We'll use the HotSpotDiagnostic MBean that we briefly introduced in the previous section. This MBean provides a dumpHeap method that accepts 2 parameters:

  • outputFile: the path of the file for the dump. The file should have the hprof extension
  • live: if set to true it dumps only the active objects in memory, as we've seen with jmap before

In the next sections, we'll show 2 different ways to invoke this method in order to capture a heap dump.

4.1. JConsole

The easiest way to use the HotSpotDiagnostic MBean is by using a JMX client such as JConsole.


As shown, we just need to introduce the parameters outputFile and live into the p0 and p1 text fields in order to perform the dumpHeap operation.

4.2. Programmatic Way

The other way to use the HotSpotDiagnostic MBean is by invoking it programmatically from Java code.

To do so, we first need to get an MBeanServer instance in order to get an MBean that is registered in the application. After that, we simply need to get an instance of a HotSpotDiagnosticMXBean and call its dumpHeap method.

Let's see it in code:

Notice that an hprof file cannot be overwritten. Therefore, we should take this into account when creating an application that prints heap dumps. If we fail to do so we'll get an exception:

5. Conclusion

In this tutorial, we've shown multiple ways to capture a heap dump in Java.

Как правило, мы должны помнить об использовании параметра HeapDumpOnOutOfMemoryError всегда при запуске приложений Java. Для других целей можно идеально использовать любые другие инструменты, если мы помним о неподдерживаемом статусе jmap.

Читайте также: