Перейти к основному содержимому

Как анализировать дампы потоков Java

· 9 мин. чтения

1. Введение

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

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

2. Потоки в JVM

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

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

Чтобы продемонстрировать тему этого руководства, мы будем использовать в качестве примера простое приложение Sender-Receiver ( NetworkDriver ) . Программа Java отправляет и получает пакеты данных, чтобы мы могли анализировать то, что происходит за кулисами.

2.1. Получение дампа потока Java

После запуска приложения существует несколько способов создания дампа потока Java для диагностики. В этом руководстве мы будем использовать две утилиты, включенные в установку JDK7+. Во- первых, мы выполним команду JVM Process Status (jps) , чтобы обнаружить процесс PID нашего приложения:

$ jps 
80661 NetworkDriver
33751 Launcher
80665 Jps
80664 Launcher
57113 Application

Во-вторых, мы получаем PID для нашего приложения, в данном случае того, что рядом с NetworkDriver. Затем мы захватим дамп потока с помощью jstack . Наконец, мы сохраним результат в текстовом файле:

$ jstack -l 80661 > sender-receiver-thread-dump.txt

2.2. Структура образца дампа

Давайте посмотрим на сгенерированный дамп потока. В первой строке отображается метка времени, а во второй — информация о JVM:

2021-01-04 12:59:29
Full thread dump OpenJDK 64-Bit Server VM (15.0.1+9-18 mixed mode, sharing):

В следующем разделе показаны внутренние потоки Safe Memory Reclamation (SMR) и не-JVM:

Threads class SMR info:
_java_thread_list=0x00007fd7a7a12cd0, length=13, elements={
0x00007fd7aa808200, 0x00007fd7a7012c00, 0x00007fd7aa809800, 0x00007fd7a6009200,
0x00007fd7ac008200, 0x00007fd7a6830c00, 0x00007fd7ab00a400, 0x00007fd7aa847800,
0x00007fd7a6896200, 0x00007fd7a60c6800, 0x00007fd7a8858c00, 0x00007fd7ad054c00,
0x00007fd7a7018800
}

Затем дамп отображает список потоков. Каждый поток содержит следующую информацию:

  • Имя: оно может предоставить полезную информацию, если разработчики включают осмысленное имя потока.
  • Приоритет (ранее): приоритет потока
  • Идентификатор Java (tid): уникальный идентификатор, присвоенный JVM.
  • Собственный идентификатор (nid): уникальный идентификатор, предоставляемый ОС, полезный для извлечения корреляции с процессором или обработкой памяти.
  • Состояние: фактическое состояние потока
  • Трассировка стека: самый важный источник информации для расшифровки того, что происходит с нашим приложением .

Мы можем видеть сверху вниз, что делают разные потоки во время моментального снимка. Давайте сосредоточимся только на интересных битах стека, ожидающих обработки сообщения:

"Monitor Ctrl-Break" #12 daemon prio=5 os_prio=31 cpu=17.42ms elapsed=11.42s tid=0x00007fd7a6896200 nid=0x6603 runnable  [0x000070000dcc5000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.SocketDispatcher.read0(java.base@15.0.1/Native Method)
at sun.nio.ch.SocketDispatcher.read(java.base@15.0.1/SocketDispatcher.java:47)
at sun.nio.ch.NioSocketImpl.tryRead(java.base@15.0.1/NioSocketImpl.java:261)
at sun.nio.ch.NioSocketImpl.implRead(java.base@15.0.1/NioSocketImpl.java:312)
at sun.nio.ch.NioSocketImpl.read(java.base@15.0.1/NioSocketImpl.java:350)
at sun.nio.ch.NioSocketImpl$1.read(java.base@15.0.1/NioSocketImpl.java:803)
at java.net.Socket$SocketInputStream.read(java.base@15.0.1/Socket.java:981)
at sun.nio.cs.StreamDecoder.readBytes(java.base@15.0.1/StreamDecoder.java:297)
at sun.nio.cs.StreamDecoder.implRead(java.base@15.0.1/StreamDecoder.java:339)
at sun.nio.cs.StreamDecoder.read(java.base@15.0.1/StreamDecoder.java:188)
- locked <0x000000070fc949b0> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(java.base@15.0.1/InputStreamReader.java:181)
at java.io.BufferedReader.fill(java.base@15.0.1/BufferedReader.java:161)
at java.io.BufferedReader.readLine(java.base@15.0.1/BufferedReader.java:326)
- locked <0x000000070fc949b0> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(java.base@15.0.1/BufferedReader.java:392)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)

Locked ownable synchronizers:
- <0x000000070fc8a668> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

На первый взгляд мы видим, что трассировка основного стека выполняет java.io.BufferedReader.readLine , что является ожидаемым поведением. Если мы посмотрим дальше, то увидим все методы JVM, выполняемые нашим приложением за кулисами . Поэтому мы можем определить корень проблемы, просмотрев исходный код или другую внутреннюю обработку JVM.

В конце дампа мы заметим несколько дополнительных потоков , выполняющих фоновые операции, такие как сборка мусора (GC) или завершение объекта :

"VM Thread" os_prio=31 cpu=1.85ms elapsed=11.50s tid=0x00007fd7a7a0c170 nid=0x3603 runnable  
"GC Thread#0" os_prio=31 cpu=0.21ms elapsed=11.51s tid=0x00007fd7a5d12990 nid=0x4d03 runnable
"G1 Main Marker" os_prio=31 cpu=0.06ms elapsed=11.51s tid=0x00007fd7a7a04a90 nid=0x3103 runnable
"G1 Conc#0" os_prio=31 cpu=0.05ms elapsed=11.51s tid=0x00007fd7a5c10040 nid=0x3303 runnable
"G1 Refine#0" os_prio=31 cpu=0.06ms elapsed=11.50s tid=0x00007fd7a5c2d080 nid=0x3403 runnable
"G1 Young RemSet Sampling" os_prio=31 cpu=1.23ms elapsed=11.50s tid=0x00007fd7a9804220 nid=0x4603 runnable
"VM Periodic Task Thread" os_prio=31 cpu=5.82ms elapsed=11.42s tid=0x00007fd7a5c35fd0 nid=0x9903 waiting on condition

Наконец, в дампе отображаются ссылки на Java Native Interface (JNI). Мы должны обратить особое внимание на это, когда происходит утечка памяти, потому что они не удаляются автоматически сборщиком мусора:

JNI global refs: 15, weak refs: 0

Дампы потоков довольно похожи по своей структуре, но мы хотим избавиться от неважных данных, сгенерированных для нашего варианта использования. С другой стороны, нам нужно будет хранить и группировать важную информацию из множества журналов, созданных трассировкой стека. Давайте посмотрим, как это сделать!

3. Рекомендации по анализу дампа потока

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

3.1. Проблемы с синхронизацией

Одним из интересных советов по фильтрации трассировки стека является состояние потока. В основном мы сосредоточимся на потоках RUNNABLE или BLOCKED и, в конечном итоге, на потоках TIMED_WAITING . Эти состояния укажут нам направление конфликта между двумя или более потоками:

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

3.2. Проблемы с выполнением

Как показывает опыт, при аномально высокой загрузке ЦП нам нужно смотреть только на RUNNABLE threads . Мы будем использовать дампы потоков вместе с другими командами для получения дополнительной информации. Одной из таких команд является top -H -p PID, которая показывает, какие потоки потребляют ресурсы ОС в данном конкретном процессе. Нам также нужно на всякий случай взглянуть на внутренние потоки JVM, такие как GC. С другой стороны, когда производительность обработки ненормально низкая , мы рассмотрим заблокированные потоки.

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

Чтобы понять эволюцию потоков с течением времени, рекомендуется делать как минимум 3 дампа, по одному каждые 10 секунд . Еще один полезный совет — разбивать дампы на небольшие куски, чтобы избежать сбоев при загрузке файлов.

3.3. Рекомендации

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

  • В вопросах выполнения сделайте несколько снимков с интервалом в 10 секунд , это поможет сосредоточиться на реальных проблемах. Также рекомендуется разделить файлы, если это необходимо, чтобы избежать сбоев при загрузке.
  • Используйте имена при создании новых потоков , чтобы лучше идентифицировать исходный код.
  • В зависимости от проблемы игнорируйте внутреннюю обработку JVM (например, GC) .
  • Сосредоточьтесь на длительных или заблокированных потоках при выдаче ненормального использования ЦП или памяти.
  • Сопоставьте стек потока с обработкой ЦП , используя top -H -p PID
  • И самое главное, используйте инструменты Analyzer

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

4. Онлайн-инструменты

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

4.1. FastThread

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

./24016a65256e9e95c283c5970679ac3a.png

FastThread включает функцию REST API для автоматизации анализа дампов потоков. С помощью простой команды cURL можно мгновенно отправить результаты. Основным недостатком является безопасность, поскольку трассировка стека хранится в облаке .

4.2. Обзор JStack

JStack Review — это онлайн-инструмент, который анализирует дампы в браузере. Это только на стороне клиента, поэтому никакие данные не хранятся вне вашего компьютера . С точки зрения безопасности это большое преимущество. Он предоставляет графический обзор всех потоков, отображая запущенные методы, а также группируя их по статусу. JStack Review отделяет потоки, создающие стек, от остальных, что очень важно, чтобы игнорировать, например, внутренние процессы. Наконец, он также включает синхронизаторы и игнорируемые строки:

./290a344eeee0a385970d92f3b548ad24.png

4.3. Spotify Online Анализатор дампа потока Java

Spotify Online Java Thread Dump Analyzer — это онлайн-инструмент с открытым исходным кодом, написанный на JavaScript. Он показывает результаты в виде обычного текста, разделяя потоки со стеком и без него. Он также отображает лучшие методы из запущенных потоков:

./5c34f49d2571fde34b9f6a75d6af0d99.png

5. Автономные приложения

Есть также несколько автономных приложений, которые мы можем использовать локально.

5.1. JProfiler

JProfiler — самый мощный инструмент на рынке, хорошо известный в сообществе разработчиков Java. Функционал можно протестировать с 10-дневной пробной лицензией. JProfiler позволяет создавать профили и прикреплять к ним запущенные приложения. Он включает в себя несколько функций для выявления проблем на месте, таких как использование ЦП и памяти и анализ базы данных. Он также поддерживает интеграцию с IDE:

./5d328289227346ed1ecdc0fe70078dc7.png

5.2. IBM Thread Monitor и анализатор дампа для Java (TMDA)

IBM TMDA можно использовать для выявления конфликтов между потоками, взаимоблокировок и узких мест. Он свободно распространяется и поддерживается, но не предлагает никаких гарантий или поддержки со стороны IBM:

./95352f21a2fe3391055d94fdcc9fea52.png

5.3. Анализатор дампа резьбы Irockel (TDA)

Irockel TDA — это автономный инструмент с открытым исходным кодом под лицензией LGPL v2.1. Последняя версия (v2.4) была выпущена в августе 2020 года, поэтому поддерживается в хорошем состоянии. Он отображает дамп потока в виде дерева, предоставляя также некоторую статистику для облегчения навигации:

./073df0cadd3ec3c38e296bedb731d2e0.png

Наконец, IDE поддерживают базовый анализ дампов потоков, что позволяет отлаживать приложение во время разработки.

5. Вывод

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

Самое главное, мы рассмотрели, как правильно их анализировать, включая рекомендации по организации огромного объема информации, встроенной в снимок.