1.简介
应用程序有时会挂起或运行缓慢,并且找出根本原因并不总是一件容易的事。线程Dump提供了一个运行Java程序的当前状态的快照**。但是,生成的数据包括多个长文件。因此,我们需要分析Java thread dump,并在大量不相关的信息中挖掘问题。
在本教程中,我们将看到如何过滤数据以有效诊断性能问题。此外,我们还将学习检测瓶颈甚至简单的错误。
2. JVM中的线程
JVM使用线程来执行每个内部和外部操作。众所周知,垃圾回收进程具有自己的线程,而且Java应用程序内部的任务也创建自己的线程。
在其生命周期中,线程会经历各种状态。每个线程都有一个跟踪当前操作的执行堆栈。此外,JVM还存储成功调用的所有先前方法。因此,可以分析整个堆栈以研究出现问题时应用程序发生了什么。
为了展示本教程的主题,我们以一个简单的Sender-Receiver
应用程序( NetworkDriver
)为例。 Java程序发送和接收数据包,因此我们将能够分析幕后发生的事情。
2.1。捕获Java线程Dump
应用程序运行后,可以通过多种方式生成用于诊断的Java线程转储。在本教程中,我们将使用JDK7 +安装中包含的两个实用程序。首先,我们将执行JVM Process Status(jps)命令来发现我们应用程序的PID进程:
$ jps
80661 NetworkDriver
33751 Launcher
80665 Jps
80664 Launcher
57113 Application
其次,我们获得应用程序的PID,在本例中为NetworkDriver.
旁边的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):
下一部分显示了安全内存回收(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
}
然后,转储显示线程列表。每个线程包含以下信息:
- 名称:如果开发人员包含有意义的线程名称,它可以提供有用的信息
- 优先级(prior):线程的优先级
- Java ID (tid):JVM给定的唯一ID
- 本机ID (nid):操作系统提供的唯一ID,可用于提取与CPU或内存处理的相关性
- 状态State:线程的实际状态
- 堆栈跟踪:最重要的信息源,可用来解释我们的应用程序正在发生的情况
我们可以从上到下看到快照时不同线程在做什么。让我们仅关注等待消息使用的堆栈中有趣的部分:
"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([email protected]/Native Method)
at sun.nio.ch.SocketDispatcher.read([email protected]/SocketDispatcher.java:47)
at sun.nio.ch.NioSocketImpl.tryRead([email protected]/NioSocketImpl.java:261)
at sun.nio.ch.NioSocketImpl.implRead([email protected]/NioSocketImpl.java:312)
at sun.nio.ch.NioSocketImpl.read([email protected]/NioSocketImpl.java:350)
at sun.nio.ch.NioSocketImpl$1.read([email protected]/NioSocketImpl.java:803)
at java.net.Socket$SocketInputStream.read([email protected]/Socket.java:981)
at sun.nio.cs.StreamDecoder.readBytes([email protected]/StreamDecoder.java:297)
at sun.nio.cs.StreamDecoder.implRead([email protected]/StreamDecoder.java:339)
at sun.nio.cs.StreamDecoder.read([email protected]/StreamDecoder.java:188)
- locked <0x000000070fc949b0> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read([email protected]/InputStreamReader.java:181)
at java.io.BufferedReader.fill([email protected]/BufferedReader.java:161)
at java.io.BufferedReader.readLine([email protected]/BufferedReader.java:326)
- locked <0x000000070fc949b0> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine([email protected]/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处理来确定问题的根源。
在dump结束位置,我们会注意到还有一些其他线程**在执行后台操作,例如垃圾回收(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
最后,dump显示Java本机接口(JNI)引用。当发生内存泄漏时,我们应该特别注意这一点,因为它们不会自动垃圾回收:
JNI global refs: 15, weak refs: 0
所有的线程dump的结构非常相似,但是我们希望摆脱为用例生成的非重要数据。另一方面,我们需要对堆栈跟踪产生的大量日志中的重要信息进行保存和分组。让我们来看看如何做!
3.分析线程转储的建议
为了了解我们的应用程序正在发生什么,我们需要有效地分析生成的快照。转储时,我们将获得很多信息**和所有线程的精确数据**。但是,我们需要整理日志文件,进行一些过滤和分组,以从堆栈跟踪中提取有用的提示。准备好转储后,我们将能够使用其他工具来分析问题。让我们看看如何解密样本转储的内容。
3.1。同步问题
过滤掉堆栈跟踪的一个有趣的技巧是线程的状态。我们将主要关注**RUNNABLE或BLOCKED线程,最后是TIMED_WAITING线程**。这些状态将使我们指向两个或多个线程之间发生冲突的方向:
- 在死锁**情况下,多个正在运行的线程在共享对像上保留一个同步块**
- 在线程争用中,当某个**线程被阻塞以等待其他线程完成时。**例如,上一节中生成的转储
3.2。执行问题
根据经验,对于异常高的CPU使用率,我们只需要查看RUNNABLE线程即可。我们将线程转储与其他命令一起使用以获取更多信息。这些命令之一是top -H -p PID,
它显示特定进程中哪些线程正在消耗OS资源。为了以防万一,我们还需要查看内部JVM线程(例如GC)。另一方面,当处理性能异常低时,我们将看一下BLOCKED线程。
在那种情况下,一个转储肯定会不足以了解正在发生的事情。为了比较不同时间的相同线程的堆栈,我们需要**在很**短**的间隔内进行大量转储**。一方面,一张快照并不总是足以找出问题的根源。另一方面,我们需要避免快照之间的干扰(信息过多)。
为了了解线程随时间的演变,建议的最佳实践是**至少进行3次dumps,每10秒进行一次**。另一个有用的技巧是将转储分成小块,以避免崩溃加载文件。
3.3。推荐建议
为了有效地理解问题的根源,我们需要在堆栈跟踪中组织大量信息。因此,我们将考虑以下建议:
- 在执行问题中,以10秒的间隔捕获几个快照将有助于重点解决实际问题。还建议根据需要拆分文件,以避免加载崩溃
- 创建新线程时使用命名以更好地识别源代码
- 根据问题,忽略内部JVM处理(例如GC)
- 发出异常的CPU或内存使用情况时,请关注**长时间运行或阻塞的线程**
- 通过使用
top -H -p PID
将线程的堆栈与CPU处理相关联 - 最重要的是,使用分析器工具
手动分析Java线程转储可能是一件乏味的工作。对于简单的应用程序,可以识别产生问题的线程。另一方面,对于复杂的情况,我们需要工具来简化此任务。在下一部分中,我们将使用为示例线程争用生成的转储,展示如何使用这些工具。
4.在线工具
有几种在线工具可用。使用此类软件时,我们需要考虑安全性问题。请记住,我们可能会与第三方实体共享日志。
4.1。快速线程
FastThread可能是分析生产环境中的线程转储的最佳联机工具。它提供了一个非常漂亮的图形用户界面。它还包括多种功能,例如线程的CPU使用率,堆栈长度以及最常用和最复杂的方法:
FastThread集成了REST API功能以自动分析线程转储。使用简单的cURL命令,可以立即发送结果。主要缺点是安全性,因为它**会将堆栈跟踪存储在云中**。
4.2。 JStack评论
JStack Review是一个在线工具,可以分析浏览器中的转储。它仅在客户端,因此没有数据存储在计算机外部。从安全角度来看,这是使用它的主要优势。它提供了所有线程的图形概述,显示了正在运行的方法,还按状态将它们分组。 JStack Review将产生堆栈的线程与其余线程分开,这对于忽略内部进程非常重要。最后,它还包括同步器和被忽略的行:
4.3。 Spotify在线Java线程转储分析器
Spotify在线Java线程转储分析器是一个用JavaScript编写的在线开源工具。它以纯文本格式显示结果,该结果以带和不带堆栈的方式分隔线程。它还显示正在运行的线程中的顶级方法:
5.独立应用程序
我们也可以在本地使用几个独立的应用程序。
5.1。 JProfiler
JProfiler是市场上最强大的工具,在Java开发人员社区中广为人知。可以使用10天的试用许可证来测试功能。 JProfiler允许创建概要文件并将正在运行的应用程序附加到它们。它包括多种功能,可以在现场识别问题,例如CPU和内存使用情况以及数据库分析。它还支持与IDE的集成:
5.2。 IBM Java线程监视器和转储分析器(TMDA)
IBM TMDA可用于识别线程争用,死锁和瓶颈。它是免费分发和维护的,但不提供IBM的任何保证或支持:
5.3。 Irockel**线程转储分析器(TDA)**
Irockel TDA是LGPL v2.1许可的独立开源工具。最新版本(v2.4)于2020年8月发布,因此维护良好。它将线程转储显示为树,还提供了一些统计信息以简化导航:
最后,IDE支持线程转储的基本分析,因此可以在开发期间调试应用程序。
5.结论
在本文中,我们演示了Java线程转储分析如何帮助我们查明同步或执行问题。
最重要的是,我们回顾了如何正确分析它们,包括对组织快照中嵌入的大量信息的建议。
0 评论