Что такое thread dump
Захват свалки java-потока
Узнайте, как захватить дамп java-потока
Захват свалки java-потока
1. Обзор
В этом учебнике мы обсудим различные способы захвата свалки потоков java-приложения.
В следующих разделах мы пройдемся по нескольким инструментам и подходам для создания свалки потоков.
2. Использование утилит JDK
2.1. jstack
jstack — это командная линия УТИЛИТА JDK, которую мы можем использовать для захвата свалки потоков. Он принимает пид процесса и отображает дампа потока в консоли. Кроме того, мы можем перенаправить его выход в файл.
Давайте рассмотрим основной синтаксис команды для захвата свалки потоков с помощью jstack:
Все флаги не являются обязательными. Давайте посмотрим, что они означают:
Давайте будем использовать эти знания, захватив дампа потока и перенаправив результат в файл:
Помните, что мы можем легко получить пид Java-процесса с помощью JPS команда.
2.2. Управление полетами Java
Java Управление полетами (JMC) — это инструмент gui, который собирает и анализирует данные из Java-приложений. После запуска JMC отображается список java-процессов, работающих на локальной машине. Мы также можем подключиться к удаленным Java-процессам через JMC.
Мы можем нажать правой кнопкой мыши на процесс и нажать на ” Начало записи полета ” вариант. После этого Темы вкладка показывает потоковые свалки:
2.3. jvisualvm
Один из его многочисленных вариантов позволяет нам захватить свалку потока. Если мы нажимаем правой кнопкой мыши на процесс Java и выбираем “Thread Dump” опция, инструмент создаст дампа потока и откроет его в новой вкладке:
2.4. jcmd
jcmd это инструмент, который работает, отправляя командные запросы в JVM. Хотя мощный, он не содержит удаленной функциональности – мы должны использовать его в той же машине, где работает процесс Java.
2.5. jconsole
jconsole позволяет нам проверить стек след каждого потока. Если мы откроем jconsole и подключиться к запущенной Java-процессу, мы можем перейти к Темы вкладка и найти след стека каждого потока :
2.6. Резюме
Как оказалось, существует множество способов захвата свалки потоков с помощью утилит JDK. Давайте на минутку поразмыслим над каждым из них и наметим их плюсы и минусы:
3. От командной линии
На серверах корпоративных приложений по соображениям безопасности устанавливается только JRE. Таким образом, мы не можем использовать вышеупомянутые утилиты, поскольку они являются частью JDK. Тем не менее, существуют различные командно-линейные альтернативы, которые позволяют нам легко захватывать свалки потоков.
Самый простой способ захвата свалки потоков в системах, похожих на Unix, — это убить команды, которую мы можем использовать для отправки сигнала в процесс с помощью убить () системный вызов. В этом случае мы отправим его в -3 сигнал.
Используя нашу ту же пид из более ранних примеров, давайте посмотрим, как использовать убить для захвата свалки потоков:
Таким образом, процесс получения сигнала Java будет печатать дампа потока на стандартном выходе.
Если мы забудем процесс Java со следующей комбинацией флагов настройки, то он также перенаправит дампа потока в данный файл:
Теперь, если мы отправим -3 сигнал, в дополнение к стандартному выходу, дамп будет доступен в В/jvm.log файл.
3.2. Ctrl и перерыв (Windows)
Обе эти команды печатают дампа потока на консоль.
4. Программное использование ThreadMxBean
В вышеупомянутой программе мы делаем несколько этапов:
5. Заключение
В этой статье мы показали несколько способов захвата свалки потоков.
Сначала мы обсуждали различные утилиты JDK, а затем альтернативы командной линии. В последнем разделе мы завершили с программным подходом с использованием JMX.
Fast thread
Universal Java Thread Dump Analyzer
HOW TO TAKE THREAD DUMPS? – 8 OPTIONS
Thread dumps are vital artifacts to diagnose CPU spikes, deadlocks, poor response times, memory problems, unresponsive applications, and other system problems. There are great online thread dump analysis tools such as http://fastthread.io/, which can analyse and spot problems. But to those tools you need provide proper thread dumps as input. Thus in this article, I have documented 8 different options to capture thread dumps.
1. jstack
‘jstack’ is an effective command line tool to capture thread dumps. jstack tool is shipped in JDK_HOMEbin folder. Here is the command that you need to issue to capture thread dump:
pid: is the Process Id of the application, whose thread dump should be captured
file-path: is the file path where thread dump will be written in to.
As per the example thread dump of the process would be generated in /opt/tmp/threadDump.txt file.
Jstack tool is included in JDK since Java 5. If you are running in older version of java, consider using other options
pid: is the Process Id of the application, whose thread dump should be captured
Note: To my knowledge this option is supported in most flavours of *nix operating systems (Unix, Linux, HP-UX operating systems). Not sure about other Operating systems.
3. JVisualVM
Java VisualVM is a graphical user interface tool that provides detailed information about the applications while they are running on a specified Java Virtual Machine (JVM). It’s located in JDK_HOMEbinjvisualvm.exe. It’s part of Sun’s JDK distribution since JDK 6 update 7.s
Launch the jvisualvm. On the left panel, you will notice all the java applications that are running on your machine. You need to select your application from the list (see the red color highlight in the below diagram). This tool also has the capability to capture thread dumps from the java processes that are running on the remote host as well.
Fig: Java Visual VM
Now go to the “Threads” tab. Click on the “Thread Dump” button as shown in the below image. Now Thread dumps would be generated.
Fig: Highlighting “Thread Dump” button in the “Threads” tab
4. JMC
Java Mission Control (JMC) is a tool that collects and analyze data from Java applications running locally or deployed in production environments. This tool has been packaged into JDK since Oracle JDK 7 Update 40. This tool also provides an option to take thread dumps from the JVM. JMC tool is present in JDK_HOMEbinjmc.exe
Once you launch the tool, you will see all the Java processes that are running on your local host. Note: JMC also can connect with java processes running on the remote host. Now on the left panel click on the “Flight Recorder” option that is listed below the Java process for which you want to take thread dumps. Now you will see the “Start Flight Recording” wizard, as shown in the below figure.
Fig: Flight Recorder wizard showing ‘Thread Dump’ capture option.
Here in the “Thread Dump” field, you can select the interval in which you want to capture thread dump. As per the above example, every 60 seconds thread dump will be captured. After the selection is complete start the Flight recorder. Once the recording is complete, you will see the thread dumps in the “Threads” panel, as shown in the figure below.
Fig: Showing captured ‘Thread Dump’ in JMC.
5. Windows (Ctrl + Break)
This option will work only in Windows Operating system.
This will generate thread dump. A thread dump will be printed on the console window itself.
Note 1: in several laptops (like my Lenovo T series) “Break” key is removedJ. In such circumstance, you have to google to find the equivalent keys for the “Break.” In my case, it turned out that “Function key + B” is the equivalent of “Break” key. Thus I had to use “Ctrl + Fn + B” to generate thread dump.s
Note 2: But one disadvantage with the approach is thread dump will be printed on the windows console itself. Without getting the thread dump in a file format, it’s hard to use the thread dump analysis tools such as http://fastthread.io. Thus when you launch the application from the command line, redirect the output a text file i.e. Example if you are launching the application “SampleThreadProgram”, you would issue the command:
instead, launch the SampleThreadProgram like this
Thus when you issue “Ctrl + Break” thread dump will be sent to C:workspacethreadDump.txtfile.
6. ThreadMXBean
Since JDK 1.5 ThreadMXBean has been introduced. This is the management interface for the thread system in the Java Virtual Machine. Using this interface also you can generate thread dumps. You only have to write few lines of code to generate thread dumps programmatically. Below is a skeleton implementation on ThreadMXBean implementation, which generates Thread dump from the application.
7. APM Tool – App Dynamics
Few Application Performance Monitoring tools provide options to generate thread dumps. If you are monitoring your application through App Dynamics (APM tool), below are the instructions to capture thread dump:
1. Create an action, selecting Diagnostics->Take a thread dump in the Create Action window.
2. Enter a name for the action, the number of samples to take, and the interval between the thread dumps in milliseconds.
3. If you want to require approval before the thread dump action can be started, check the Require approval before this Action checkbox and enter the email address of the individual or group that is authorized to approve the action. See Actions Requiring Approval for more information.
4. Click OK.
Fig: App dynamics thread dump capturing wizard
8. JCMD
The jcmd tool was introduced with Oracle’s Java 7. It’s useful in troubleshooting issues with JVM applications. It has various capabilities such as identifying java process Ids, acquiring heap dumps, acquiring thread dumps, acquiring garbage collection statistics, ….
Using the below JCMD command you can generate thread dump:
pid: is the Process Id of the application, whose thread dump should be captured
file-path: is the file path where thread dump will be written in to.
As per the example thread dump of the process would be generated in /opt/tmp/threadDump.txt file.
Conclusion
a. Simple (straightforward, easy to implement)
b. Universal (works in most of the cases despite OS, Java Vendor, JVM version, …)
Многопоточность в Java. Лекция 6: взаимные блокировки и дампы потоков
В пятой лекции краткого курса о многопоточности речь шла об атомарных переменных и многопоточных коллекциях. На этот раз наши коллеги рассказывают о дампах потоков, простых и скрытых дедлоках.
В некорректно спроектированной многопоточной программе может возникнуть ситуация, когда два потока блокируют друг друга. В этом случае их выполнение зависает, пока программу не остановят извне. Такая ситуация называется deadlock.
6.1 Дампы потоков
Помимо дедлоков, бывает, что поток очень долго ожидает какие-то ресурсы или остается постоянно активным, например, выполняя очень большой или бесконечный цикл. Для выявления таких ситуаций и других проблем, связанных с потоками, JVM предоставляет возможность сделать мгновенный снимок состояния потоков. Такой снимок называется thread dump. Он представляет собой текстовый документ, в котором перечислены все потоки, в том числе, потоки JVM. Для каждого потока отображается стандартный набор информации: имя, статус, приоритет, стек-трейс, демон ли поток или нет, а также адрес объекта блокировки, на которой находится поток. Часть такого thread dump приведена в листинге 1.
Листинг 1
«Java Thread» #11 prio=5 os_prio=0 tid=0x00007fb0a4356000 nid=0x1242 waiting for monitor entry [0x00007fb078701000] java.lang.Thread.State: BLOCKED (on object monitor)
at com.da.lect5.deadlock.TwoTasks.lambda$getTask1$0(TwoTasks.java:14)
— waiting to lock (a java.lang.String)
— locked (a java.lang.String)
at com.da.lect5.deadlock.TwoTasks$$Lambda$1/1078694789.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
«UNIX Thread» #12 prio=5 os_prio=0 tid=0x00007fb0a4357800 nid=0x1243 waiting for monitor entry [0x00007fb078600000] java.lang.Thread.State: BLOCKED (on object monitor)
at com.da.lect5.deadlock.TwoTasks.lambda$getTask2$1(TwoTasks.java:27)
— waiting to lock (a java.lang.String)
— locked (a java.lang.String)
at com.da.lect5.deadlock.TwoTasks$$Lambda$2/1747585824.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
«Monitor Ctrl-Break» #5 daemon prio=5 os_prio=0 tid=0x00007fb0a42b5800 nid=0x123b runnable [0x00007fb07901f000] java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
— locked (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
— locked (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
Существует несколько способов снять thread dump:
В листинге 1 можно увидеть, что поток с именем Java Thread заблокирован на мониторе с адресом 0x0000000719bf5760. Важно правильно сопоставить адрес объекта с самим объектом, потому что по шестнадцатеричному значению сделать это невозможно. Для этого можно использовать код, приведенный в листинге 2.
Листинг 2:
Используя класс в листинге 2, можно понять, какой поток на какой блокировке находится. При использовании этого класса следует учесть, что адреса объектов могут меняться после работы сборщика мусора. При анализе потоков необходимо фильтровать потоки, которые создал пользователь, и те, которые запустила сама JVM. Поэтому удобно назначать имена потокам, как это было показано в лекции номер 2. Рекомендуется делать дампы потоков работающего приложения несколько раз, чтоб увидеть изменения состояния потоков. Если одно из ядер процессора загружено на 100 %, следует искать бесконечный цикл или цикл, который очень долго выполняется, обрабатывая большое количество данных. Если предельной загрузки процессора не наблюдается, но какая-то работа все же ожидает выполнения, значит, возник один из видов дедлока или потоки ждут освобождения определенного ресурса.
6.2 Простая взаимная блокировка
Простой дедлок возникает, когда из двух потоков первый захватил блокировку А и пытается захватить блокировку B, а второй захватил блокировку B и пытается захватить блокировку A. Пример такого дедлока приведен в листинге 3.
Листинг 3:
public class DeadLock <
public static void main(String[] args) <
TwoTasks tasks = new TwoTasks();
new Thread(tasks.getTask1(), «Java Thread»).start();
new Thread(tasks.getTask2(), «UNIX Thread»).start();
>
>
В листинге 1 создаются два потока: первый сначала захватывает блокировку на строке str1, а затем — на str2. Второй поток делает то же самое, только в другом порядке. Два потока пытаются захватить блокировки бесконечное количество раз. Рано или поздно наступит дедлок: когда первый поток захватил блокировку на строке “Java” и хочет захватить блокировку на строке “UNIX”. А второй поток уже захватил блокировку на строке “UNIX” и пытается захватить блокировку на строке “Java”. В результате программа в Листинге 1 будет находиться в состоянии взаимной блокировки вечно — т. е. до тех пор пока ее не остановят. Решение в сложившейся ситуации — использовать один и тот же порядок захвата и отпускания блокировок во всех критических секциях программы.
Не стоит использовать в качестве объектов блокировки строки. Это связано с тем, что JVM кэширует строки, объявленные при помощи литералов. Соответственно, строки с одинаковым содержанием будут ссылаться на один и тот же объект, хотя могут быть объявлены в разных частях программы.
6.3 Скрытый дедлок
В разделе 6.1 был рассмотрен случай взаимной блокировки, который виртуальная Java-машина смогла определить, что и было показано в thread dump. Однако могут возникать ситуации, когда Java-машина определить дедлок не может. Рассмотрим такую программу в листинге 4.
Листинг 4:
public class LockOrderingDeadlockSimulator <
public static void main(String[] args) <
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch endSignal = new CountDownLatch(3);
TasksHolder tasks = new TasksHolder();
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new WorkerThread1(tasks, startSignal, endSignal));
executor.execute(new WorkerThread2(tasks, startSignal, endSignal));
Runnable deadlockDetector =
new ThreadDeadlockDetector(tasks, startSignal, endSignal);
executor.execute(deadlockDetector);
executor.shutdown();
while (!executor.isTerminated()) <
try <
endSignal.await();
> catch (InterruptedException e) <
>
>
public class TasksHolder <
private final Object SHARED_OBJECT = new Object();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void executeTask1() <
// 1. Attempt to acquire a ReentrantReadWriteLock READ lock
lock.readLock().lock();
// Wait 2 seconds to simulate some work.
try <
Thread.sleep(2000);
> catch (InterruptedException any) <
>
try <
// 2. Attempt to acquire a Flat lock.
synchronized (SHARED_OBJECT) <
>
> finally <
lock.readLock().unlock();
>
System.out.println(«executeTask1() :: Work Done!»);
>
public void executeTask2() <
// 1. Attempt to acquire a Flat lock
synchronized (SHARED_OBJECT) <
// Wait 2 seconds to simulate some work.
try <
Thread.sleep(2000);
> catch (InterruptedException any) <
>
// 2. Attempt to acquire a ReentrantReadWriteLock WRITE lock
lock.writeLock().lock();
try <
// Do nothing
> finally <
lock.writeLock().unlock();
>
>
System.out.println(«executeTask2() :: Work Done!»);
>
public ReentrantReadWriteLock getReentrantReadWriteLock() <
return lock;
>
>
public class WorkerThread1 implements Runnable <
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
private TasksHolder tasks;
public WorkerThread1(TasksHolder tasks, CountDownLatch startSignal,
CountDownLatch endSignal) <
this.tasks = tasks;
this.startSignal = startSignal;
this.endSignal = endSignal;
>
@Override
public void run() <
try <
startSignal.await();
// Execute task #1
tasks.executeTask1();
> catch (InterruptedException e) <
> finally <
endSignal.countDown();
>
>
>
public class WorkerThread2 implements Runnable <
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
private TasksHolder tasks;
public WorkerThread2(TasksHolder tasks, CountDownLatch startSignal,
CountDownLatch endSignal) <
this.tasks = tasks;
this.startSignal = startSignal;
this.endSignal = endSignal;
>
@Override
public void run() <
try <
startSignal.await();
// Execute task #2
tasks.executeTask2();
> catch (InterruptedException e) <
> finally <
endSignal.countDown();
>
>
>
public class CommonResource <
private Worker owner;
public CommonResource (Worker d) <
owner = d;
>
public Worker getOwner () <
return owner;
>
public synchronized void setOwner (Worker d) <
owner = d;
>
>
public class Worker <
private String name;
private boolean active;
private final Object LOCK = new Object();
public Worker (String name, boolean active) <
this.name = name;
this.active = active;
>
public String getName () <
return name;
>
public boolean isActive () <
return active;
>
Как анализировать Thread Dump
В программе курса Разработчик Java довольно много тем, посвященных внутренностям работы JVM. Мы разбираемся в механизмах работы коллекций, байт-кода, сборщика мусора и т.д. Сегодня предлагаем Вашему внимаю перевод довольно интересной статьи о thread dump-е. Что это такое, как его получить и как использовать.
Хотите узнать, как анализировать thread dump (дамп потоков)? Заходите под кат, чтобы узнать больше о том как в Java получить thread dump и что с ним потом делать.
Большинство современных Java-приложений являются многопоточными. Многопоточность может существенно расширить функционал приложения, в то же время она вносит существенную сложность.
В однопоточном приложении все ресурсы (разделяемая память, операции ввода/вывода и т.д.) могут использоваться без синхронизации, т.к. в любой момент времени только один поток пользуется ресурсом.
В случае многопоточных приложений необходимо найти компромисс между усложнением программы и возможным повышением производительности, когда несколько потоков могут использовать все доступные (часто больше одного) ядра центрального процессора (CPU). Если сделать все правильно, то используя многопоточность (формализована в Amdahl’s Law), можно добиться существенного прироста производительности приложения. Однако при этом надо помнить об обеспечении синхронного доступа нескольких потоков к разделяемому ресурсу. В большинстве случаев, фреймворки, такие как Spring, инкапсулируют работу с потоками и скрывают от пользователей многие технические детали. Однако и в случае применения современных сложных фреймворков что-то может пойти не так, и мы, как пользователи, столкнемся со сложно решаемыми багами многопоточности.
К счастью, Java оснащена специальным механизмом для получения информации о текущем состоянии всех потоков в любой момент времени — это thread dump (своего рода моментальный снимок). В этой статье мы изучим, как получить thread dump для приложения реалистичных размеров и как этот dump проанализировать.
Предполагается, что читатель владеет базовой информацией о многопоточном программировании и знает о проблемах синхронизации потоков и использовании совместных ресурсов. Тем не менее будет не лишним освежить в памяти некоторые основные термины и понятия.
Основная терминология
С первого взгляда Java thread dump-ы могут показаться «китайской грамотой», ключом к ее понимаю являются следующие понятия. В общем, давайте, повторим основные термины многопоточности, которые будем использовать для анализа дампов.
У каждого потока есть уникальный идентификатор и имя. Потоки могут быть «демонами» и «не демонами».
Программа завершает свою работу, когда завершаются все потоки «не демоны» или вызывается метод Runtime.exit. Работающие «демоны» не влияют на завершение работы программы. Т.е. JVM ждем когда доработают все «не демоны» и завершает работу, на «не демонов» не обращает внимание.
За более подробной информацией обращайтесь к документации класса Thread.
Поток может находится в одном из следующих состояний:
Каждый объект в 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@ будут разные для каждого запуска):
После вывода этих сообщений программа будет выглядеть как работающая (процесс, выполняющий эту программу, не завершен), при этом программа не выполняет никакой работы. Вот так вот и выглядит deadlock на практике. Чтобы решить проблему, нам надо вручную создать tread dump и проанализировать состояние потоков.
Генерация 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». Все зависит от того, как приложение было запущено.
Полученный файл thread_dump.txt содержит thread dump нашей зависшей программы и содержит важную информацию для диагностики причин возникновения deadlock-а.
В нашем случае вызов будет такой:
Анализ простого Thread Dump
Открыв файл thread_dump.txt, мы увидим примерно следующее содержание:
Introductory Information
Хотя на первый взгляд этот файл может показаться слишком сложным и запутанным, в действительности он весьма прост, если разбирать его по частям шаг за шагом.
В первой строке указывается время, когда дамп был сформирован, во второй — диагностическая информация о JVM, на которой дамп был получен:
В этой секции нет информации о потоках. Тут задается общий контекст системы, в которой был собран дамп.
Общие сведенья о потоках
В следующей секции представлена информация о потоках, которые выполнялись в системе в момент сбора дампа:
В следующей секции приводится список:
В нем содержится информация о потоках за пределами JVM, т.е. это не потоки виртуальной машины и не потоки сборщика мусора. Если посмотреть на адреса этих потоков, то можно заметить, что они соответствуют значению tid — «естественному, железному» (native) адресу в операционной системе, а не Thread ID.
Троеточия используются для сокрытия излишней информации:
Потоки
Сразу после блока SMR следует список потоков. Первый поток в нашем списке — Reference Handler:
Краткое описание потока
В первой строке для каждого потока приводится общее описание. Описание содержит следующие пункты:
Раздел | Пример | Описание |
---|---|---|
Name | «Reference Handler» | Человеко-читаемое имя потока. Имя можно задать, вызвав метод setName объекта Thread. А получить через вызов getName |
ID | #2 | Уникальный ID, присвоенный каждому объекту класса Thread. ID генерируется для потоков в системе. Первоначальное значение 1. Каждому вновь созданному потоку присваивается свой ID, предварительно увеличенный на 1. Это свойство потока «только для чтения», может быть получено с помощью функции getId объекта класса Thread. |
Daemon status | daemon | Флаг является признаком того, что поток является демоном. Если это демон, то флаг будет выставлен. Например, поток Thread-0 не демон |
Priority | prio=10 | Числовой приоритет Java-потока. Обратите внимание, что этот приоритет не обязательно соответствует приоритету связанного потока в операционной системе. Для установки приоритета можно воспользоваться методом setPriority объекта класса Thread, а для получения методом getPriority. |
OS Thread Priority | os_prio=2 | Приоритет потока в операционной системе. Этот приоритет может отличаться от того, который присвоен связному Java-потоку. |
Address | tid=0x00000250e4979000 | Адрес Java-потока. Этот адрес представляет собой указатель на Java Native Interface (JNI) нативный объект класса Thread (объект C++ Thread который связан с Java-потоком через JNI). Это значение получается путем приведения указателя на this (объекта C++, который связан с этим Java-потоком) к integer. См. строку 879 в hotspot/share/runtime/thread.cpp: Хотя ключ для этого объекта (tid) может быть похож на ID-потока, |
OS Thread ID | nid=0x3c28 | Уникальный идентификатор потока операционной системы, к которому привязан Java-поток. Это значение выводится следующим кодом: строка 42 в hotspot/share/runtime/osThread.cpp: |
Status | waiting on condition | Человеко-читаемый статус текущего потока. Эта строка выводит дополнительную информацию к простому статусу потока (см.ниже), которая может быть использована для понимания, что поток собирался делать (т.е. пытался ли поток получить блокировку или ждал выполнения условия разблокирования). |
Last Known Java Stack Pointer | [0x000000b82a9ff000] | Последний известный указатель на стек (SP), ассоциированный и данным потоком. Это значение получается с использованием нативного кода С++, перемешанного с кодом Java с использованием JNI. Значение возвращает функция last_Java_sp(), строка 2886 в hotspot/share/runtime/thread.cpp: Для простых thread dump-ов эта информация практически бесполезна. Однако в сложных случаях SP может |
Состояние потока
Вторая строка — это текущее состояние потока. Возможные состояния потока приведены в 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) потоки
Последний раздел содержит информацию о найденных взаимных блокировках (deadlocks).
Если такие не обнаружатся, то и раздел будет пустым. Т.к. мы специально разработали приложение с блокировками, в нашем случае этот раздел есть. Блокировка обнаружена во время формирования дампа и представлена следующим сообщением:
В первом подразделе описывается сценарий взаимной блокировки (deadlock):
Поток Thread-0 ожидает возможность захватить монитор (это обращение к блоку synchronized(secondResource) в нашем приложении), в то же время этот поток удерживает монитор, который пытается захватить поток Thread-1 (это обращение к тому же фрагменту кода: synchronized(secondResource) в нашем приложении).
Эта циклическая блокировка по другому называется deadlock. На рисунке ниже
эта ситуация представлена в графическом виде:
Во втором подразделе для обоих заблокированных потоков приведен stack trace.
Этот stack trace позволяет нам проследить за работой каждого потока до появления блокировки.
В нашем случае, если мы посмотрим на строку:
at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34), то увидим проблемный участок кода:
Эта строка является первой строкой блока synchronized, который является причиной блокировки, и подсказывает нам, что синхронизация на secondResource и является причиной взаимной блокировки. Чтобы исправить ситуацию, мы должны обеспечить в обоих потоках одинаковый порядок синхронизации на ресурсах resourceA и resourceB. Если мы сделаем это, то придем к следующему приложению:
Это приложение завершится без взаимной блокировки, и в качестве результата мы получим следующий вывод (обратите внимание на то, что адреса экземпляров класса Object изменились):
В результате, используя только информацию, полученную из thread dump, мы смогли найти причину и исправить взаимную блокировку в нашем приложении. Предложенная техника достаточна для большинства простых приложений (или приложений с небольшим количеством deadlock-ов). Чтобы разобраться с более сложными ситуациями, потребуются другие методы.
Анализ более сложных Thread Dump-ов
Дампы настоящих приложений могут быть очень большими и сложными.
В одной JVM одновременно могут быть запущены сотни тысяч потоков. И заблокированными могут оказаться больше чем два потока (или может быть несколько проблем многопоточности, вызванных одной проблемой).
Анализ такого громадного объема информации может стать настоящей проблемой.
Для анализа больших дампов предназначены специальные утилиты-анализаторы — Thread Dump Analyzers (TDAs). Эти утилиты парсят Java thread dump-ы и выводят информацию в человеко-читаемом виде, часто с применением графических средств. Более того, некоторые из них могут выполнить статический анализ и найти причину проблемы. Конечно, выбор конкретной утилиты зависит от целого ряда обстоятельств.
Тем не менее приведем список наиболее популярных TDA:
Заключение
Thread dump-ы — это отличное средство анализа состояния Java-приложения, особенно в случаях неожиданного поведения многопоточных приложений. Однако без надлежащего багажа знаний, дампы могут внести дополнительную сложность и в без того сложную ситуацию.
В этой статье мы разработали приложение с deadlock, сформировали дамп зависшего приложения. Проанализировали дамп и нашли причину блокировки и исправили её. Это не всегда так просто, для анализа большинства реальных приложений очень полезно использовать специальные утилиты — анализаторы.
Тем не менее, каждый профессиональный Java-разработчик должен понимать основы анализа thread dump-ов. Должен ориентироваться в их структуре, знать какую информацию можно извлечь и как ее использовать для решения проблем многопоточности.
Хотя, thread dump — это не «серебряная пуля» в мире многопоточности, тем не менее это важное средство диагностирования сложных, но довольно распространенных проблем многопоточных Java-приложений.
В программе курса Разработчик Java вопросы многопоточности занимают заметную часть. Мы детально рассматриваем как разрабатывать программы так, чтобы не приходилось по ночам разбираться с deadlock-в продакшене.
Как всегда интересны ваши мнения и комментарии, которые можно оставить тут или заглянуть к Виталию на день открытых дверей.