如何分析 Java 线程转储日志
一、简介
开发过程中,有时候应用程序进程会挂掉或运行缓慢,但要确定其根本原因比较困难。线程转储日志 提供了当前 Java 进程**的细节快照**。但是,转储生成的日志文件很大。因此,我们需要有一定的分析技巧,以便从大量的线程转储信息中发现问题。
在本文中,我们将看到如何过滤掉无用数据以有效诊断性能问题。此外,我们将学习如何检测性能瓶颈甚至是简单的错误。
2. JVM 中的线程
JVM 使用线程来执行每个内外部操作。另外,垃圾收集进程有自己的线程,并且 Java 应用程序内部的任务也会创建自己的线程。
在其生命周期中,Java 线程会经历各种状态。每个线程都有一个跟踪当前操作的执行堆栈。此外,JVM 还存储了之前成功调用的所有方法。因此,可以通过分析完整的堆栈信息,以研究问题出现时应用程序发生了什么。
我们将使用一个简单的发送-接受应用程序 ( NetworkDriver )作为示例。我们的 Java 程序将会发送和接收数据包,我们来分析幕后发生的事情。
2.1. 捕获 Java 线程转储
应用程序运行后,有多种方法可以生成用于问题诊断的 Java 线程转储文件。在本文中,我们将使用 JDK 自带的命令。首先,我们将执行 JPS 命令来找到应用程序的进程 ID:
$ jps 80661 NetworkDriver 33751 Launcher 80665 Jps 80664 Launcher 57113 Application
其次,我们获取应用程序的进程 ID(PID),在本例中,是NetworkDriver旁边的 PID(80661)。然后,我们将使用 jstack 命令转储线程日志。最后,我们将结果存储在一个文本文件中:
$ jstack -l 80661 > sender-receiver-thread-dump.txt
2.2. 转储日志的结构
让我们看看生成的转储日志。第一行显示时间戳,而第二行打印了 JVM 的信息:
接下来一段显示安全内存回收 (SMR) 和非 JVM 内部线程:
然后,日志会显示线程列表。每个线程包含以下信息:
- 名词(Name): 如果开发者设置了一个有意义的线程名称,该字段就可以方便定位
- 优先级(prior):线程的优先级
- Java ID(tid):JVM 赋予线程的唯一 ID
- Native ID (nid):操作系统赋予线程的唯一 ID,用于找出线程与 CPU/内存处理的相关性
- state:线程 的实际状态
- 堆栈信息:用于定位应用程序正在发生的事情,日志中最重要的信息来源
我们可以从上到下看到快照时不同线程在做什么。这里我们只关心堆栈中等待消费消息的部分:
乍一看,我们看到主堆栈跟踪正在执行 java.io.BufferedReader.readLine,这是我们预期的行为。如果我们往下看,我们会看到应用程序在幕后执行的所有 JVM 方法。因此,我们可以通过查看源代码或其他 JVM 内部操作来确定问题的根源。
在日志末尾,我们会注意到有几个额外的线程在执行后台操作,例如垃圾回收 (GC) 线程:
最后,日志显示了 Java 本地接口 (JNI) 引用。当发生内存泄漏时,我们应该特别注意这块,因为它们不会被自动垃圾收集:
线程转储日志的结构是固定的,但分析问题时我们想过滤次要数据。另一方面,我们需要从堆栈跟踪产生的大量日志中保存和分组重要信息。让我们看看怎么做吧!
3. 分析转储日志的建议
为了了解我们的应用程序发生了什么,我们需要有效地分析生成的转储日志。我们将获得大量信息以及转储时所有线程的精确数据。但是,我们需要整理日志文件,进行一些过滤和分组以从堆栈中提取有用信息。准备好转储日志后,我们就能够使用不同的工具分析它。
3.1. 线程同步问题
过滤堆栈信息的一个有趣技巧是线程的状态。我们将主要关注 RUNNABLE 或 BLOCKED 线程,最后可以关注下 TIMED_WAITING 线程 。这些状态将我们指向两个或多个线程之间的竞争:
- 在死锁情况下,运行的多个线程在共享对象上持有同步块
- 在线程竞争中,一个线程被阻塞等待其他线程完成。比如上面的例子
3.2. 线程执行问题
根据经验,对于异常高的 CPU 使用率,我们只需要查看 RUNNABLE 线程。我们将使用一些命令从日志获取额外信息。其中一个命令是top -H -p PID,它显示在该特定进程中哪些线程正在消耗操作系统资源。为了以防万一,我们还需要查看内部 JVM 线程,例如 GC。另一方面,当处理性能异常低时,我们将查看 BLOCKED 线程。
在这些情况下,单次转储日志肯定不足以了解正在发生的事情。我们需要在很近的时间间隔内进行多次转储,以便比较不同时间相同线程的堆栈。一方面,一个快照并不总是足以找出问题的根源。另一方面,我们需要避免快照之间的冗余信息。
要了解线程随时间的演变,建议的最佳实践是至少进行 3 次转储,每 10 秒一次。另一个有用的技巧是将转储分成小块以避免加载文件时崩溃。
3.3. 建议
为了有效地定位问题的根源,我们需要整理堆栈日志中的大量信息。因此,我们将考虑以下建议:
- 在执行问题中,每隔 10 秒抓几张快照,将有助于定位实际问题。如果需要,还建议拆分文件以避免加载崩溃
- 创建新线程时使用良好的命名以更好地识别您的源代码
- 根据问题,忽略内部 JVM 处理(例如 GC)
- 出现异常 CPU 或内存使用时,重点关注长时间运行或阻塞的线程
- 使用top -H -p PID**将线程的堆栈与 CPU 处理时间相关联**
- 最重要的是,使用分析工具
手动分析 Java 线程转储可能是一项乏味的活动。对于简单的应用程序,很多时候是可以识别产生问题的线程。但对于复杂的情况,我们还是需要工具来简化这项工作。我们将在接下来的部分中展示如何使用这些工具。
### 4**. 工具**
我们可以在本地使用独立的应用程序,这里仅介绍比较出名的JProfiler。
JProfiler
JProfiler是市场上最强大的工具,在 Java 开发人员社区中广为人知。可以使用 10 天试用许可证测试功能。JProfiler 允许创建配置文件并关联运行的应用程序。它包括多种功能,可在现场识别问题,例如 CPU 和内存使用情况以及数据库分析。它还支持与 IDE 的集成:
#学习路径##Java#