如何分析 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 的信息:

img

接下来一段显示安全内存回收 (SMR) 和非 JVM 内部线程:

img

然后,日志会显示线程列表。每个线程包含以下信息:

  • 名词(Name): 如果开发者设置了一个有意义的线程名称,该字段就可以方便定位
  • 优先级(prior):线程的优先级
  • Java ID(tid):JVM 赋予线程的唯一 ID
  • Native ID (nid):操作系统赋予线程的唯一 ID,用于找出线程与 CPU/内存处理的相关性
  • state:线程 的实际状态
  • 堆栈信息:用于定位应用程序正在发生的事情,日志中最重要的信息来源

我们可以从上到下看到快照时不同线程在做什么。这里我们只关心堆栈中等待消费消息的部分:

img

乍一看,我们看到主堆栈跟踪正在执行 java.io.BufferedReader.readLine,这是我们预期的行为。如果我们往下看,我们会看到应用程序在幕后执行的所有 JVM 方法。因此,我们可以通过查看源代码或其他 JVM 内部操作来确定问题的根源。

在日志末尾,我们会注意到有几个额外的线程在执行后台操作,例如垃圾回收 (GC) 线程

img

最后,日志显示了 Java 本地接口 (JNI) 引用。当发生内存泄漏时,我们应该特别注意这块,因为它们不会被自动垃圾收集:

img

线程转储日志的结构是固定的,但分析问题时我们想过滤次要数据。另一方面,我们需要从堆栈跟踪产生的大量日志中保存和分组重要信息。让我们看看怎么做吧!

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#
全部评论
点赞 回复 分享
发布于 2021-09-15 22:28

相关推荐

小红书 后端选手 n*16*1.18+签字费期权
点赞 评论 收藏
分享
点赞 评论 收藏
分享
微风不断:兄弟,你把四旋翼都做出来了那个挺难的吧
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-27 10:48
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务