Java Thread Dump 分析

诊断 Java 应用的过程中, 我们经常要去看线程的相关信息. 比如当前 JVM 有多少线程, 从启动到现在已经创建过多少线程, 线程的 CPU 使用率, 线程是不是发生了死锁, CPU 使用率过高是哪些代码导致的. 这要求我们对 Java 的线程必须有一定的了解, 才能比较有针对性的分析这些问题. 下面我们就从分析 Thread dump 开始介绍 Thread 相关的信息.

如何查看当前Java应用线程的信息

最快速的方式就是获得当前应用的 Thread dump, 然后使用文本的方式查看. 比如:

$ jps
49048 Jps
3499 Pycharm
$ jcmd 3499 Thread.print > /tmp/thread.txt
$ cat /tmp/thread.txt
复制代码

上面的例子中, 我们先是使用 jps 查看当前的用户的 Java 进程, 然后我们使用 jcmd <pid> Thread.print 命令来输出 Thread dump. 默认情况下, 这个命令会打印到当前控制台, 我们重定向到一个本地文件. 然后使用 cat 命令来查看具体的线程内容(如果你是桌面模式, 可以使用任何文本编辑器查看).

我们把整个 Thread dump 的信息, 分成3部分来分别讲解:

  1. 头部 JVM 及线程 Summary 部分
  2. 常规线程具体信息部分
  3. 尾部特殊线程及其它部分

Thread dump 头部 JVM 及线程ID列表

在最开始部分, 我们看到类似下面的信息:

3499:
2023-04-01 08:41:33
Full thread dump OpenJDK 64-Bit Server VM (17.0.5+1-b653.25 mixed mode):

Threads class SMR info:
_java_thread_list=0x000060000b029f00, length=113, elements={
0x00007fc18981e800, 0x00007fc18a01ee00, 0x00007fc18a01cc00, 0x00007fc18a8bea00,
				... ...
0x00007fc1878f0200, 0x00007fc1410d6600, 0x00007fc18a489600, 0x00007fc131aefa00,
0x00007fc131ad4a00
}
复制代码

第一行是当前进程的进程号(pid), 第二行是产生 Thread dump的系统时间, 第三行主要是JDK的信息, 然后是所有线程的线程ID(tid)的列表,以及线程的数量(113). 上面SMR是 Safe Memory Reclamation 的缩写. 这部分信息通常情况下价值不大, 大部分有用的信息都在线程具体内容部分.

Thread dump 常规线程具体信息

接下来就是每个线程的具体元数据部分和线程栈部分. 我们拿其中一个例子来说明:

"pool-4-thread-1" #191 prio=5 os_prio=31 cpu=4012.76ms elapsed=313903.17s allocated=1229K defined_classes=23 tid=0x00007fc1898a8000 nid=0x23757 in Object.wait()  [0x000070000a4d5000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait(java.base@17.0.5/Native Method)
	- waiting on <no object reference available>
	at java.io.PipedInputStream.read(java.base@17.0.5/PipedInputStream.java:326)
	- eliminated <0x0000000782139b00> (a java.io.PipedInputStream)
	at java.io.PipedInputStream.read(java.base@17.0.5/PipedInputStream.java:377)
	- locked <0x0000000782139b00> (a java.io.PipedInputStream)
	at com.jetbrains.python.console.transport.TCumulativeTransport.read(TCumulativeTransport.kt:46)
	at com.jetbrains.python.console.transport.server.TNettyServerTransport$TNettyTransport.readAll(TNettyServerTransport.kt:243)
	at org.apache.thrift.protocol.TBinaryProtocol.readAll(TBinaryProtocol.java:455)
	at org.apache.thrift.protocol.TBinaryProtocol.readI32(TBinaryProtocol.java:354)
	at org.apache.thrift.protocol.TBinaryProtocol.readMessageBegin(TBinaryProtocol.java:243)
	at org.apache.thrift.TBaseProcessor.process(TBaseProcessor.java:27)
	at org.apache.thrift.server.TThreadPoolServer$WorkerProcess.run(TThreadPoolServer.java:313)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.5/ThreadPoolExecutor.java:1136)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.5/ThreadPoolExecutor.java:635)
	at java.lang.Thread.run(java.base@17.0.5/Thread.java:833)

   Locked ownable synchronizers:
	- <0x0000000782139ef8> (a java.util.concurrent.ThreadPoolExecutor$Worker)
复制代码

第一行是当前线程的元数据信息, 包含很多有用的信息, 我们逐一来解释一下:

  1. "pool-4-thread-1": 是当前线程的名字
  2. #192: 表示这是应用启动后创建的第192个线程, 我们可以看到这个数字在我们的 Thread dump 从上到下是顺序增加的, 如果你发现中间某个数字没有, 说明那个线程已经死亡了.
  3. prio=5: 这是线程的优先级. Java 线程优先级是1~10, 默认是5.
  4. os_prio=31: 这是当前线程在操作系统层面的优先级, 这里是31.
  5. cpu=4012.76ms: 表示当前线程从开始运行到生成Thread dump 时共使用了4012.76ms CPU 时间.
  6. elapsed=313903.17s: 表示当前线程已经存活了313903.17s秒.
  7. allocated=1229K: 表示当前线程共分配了1229K的内存.
  8. defined_classes=23: 表示当前线程共新定义了23个新类.
  9. tid=0x00007fc1898a8000: 当前线程的ID, 和第一部分的线程ID对应.
  10. nid=0x23757: 当前线程在操作系统层面的ID(在Linux里,线程是一个轻量级的进程(LWP)).
  11. in Object.wait(): 表示当前线程最后运行在 Obect 类的 wait() 方法里面.
  12. [0x000070000a4d5000]: 最后的栈指针(SP)地址, 一般诊断中用不到这个信息.

第二行告诉我们当前线程的状态: TIMED_WAITING, 并且是等着对象的 monitor 上.

接着是线程的栈信息, 以 at 开头的行告诉我们当前栈帧执行的类和方法, 括号里面的内容告诉我们这个类的源文件名及当前在执行的行号, 如果前面有 /, 斜线前面表示代码属于的模块及版本.

另外有些行是以 - 开头的, 比如下面的每一行的栈帧都跟着一行以 - 开头的一行:

	at java.lang.Object.wait(java.base@17.0.5/Native Method)
	- waiting on <no object reference available>
	at java.io.PipedInputStream.read(java.base@17.0.5/PipedInputStream.java:326)
	- eliminated <0x0000000782139b00> (a java.io.PipedInputStream)
	at java.io.PipedInputStream.read(java.base@17.0.5/PipedInputStream.java:377)
	- locked <0x0000000782139b00> (a java.io.PipedInputStream)
复制代码

首先, 我们最下面2行看起, 第5行表示在执行 PipedInputStream 的 377 行的 read()方法的时候, 当前线程获得了一把锁, 这把锁是属于一个 java.io.PipedInputStream 对象的. 下面是截取的 JDK 这个版本的 PipedInputStream 类的377 行附近的源代码:

可以看到, 这个方法是一个 synchronized 方法, 要执行到377行, 必须先获得当前对象的锁.

然后我们看第3, 4 行, 它表示在执行PipedInputStream 的326 行的 read() 方法的时候, 当前线程临时释放(eliminated) 了上层栈帧(第5行)获得的那把锁, 根据锁的ID可以看出锁是同一把.

然后我们看326行附近的源代码:

进入这个方法也需要获得当前对象的锁, 但是上层栈帧已经获得了这个锁, 所以这次可以直接进入这个方法. 然后在326行, 当前线程调用当前对象的 wait() 方法, 并且给了1000ms 的参数, 意思是: 当前线程要临时放弃拥有这个锁, 并且加入这个锁的wait队列. 在这个wait队列里, 如果没被 notify()notifyAll() 提前唤醒进入等待队列, 那么至多等待1000ms, 就可以进入这个锁的block队列(关于 syncronized 锁, wait(), notify(), notifyAll() 方法以及 block队列 和 wait 队列, 请查询相关信息), 然后就可以竞争再次获得这个锁.

进入 wait() 方法之后, 就进入了非Java写的 native code, 拿不到对象的地址, 所以这里虽然显示等着, 但是没有对象的引用可以给我们看.

最后, 每个线程尾部, 都有一段是关于Locked ownable synchronizers的部分. 有的线程拥有这种一个或多个同步器, 有的没有(none), 如果有就显示在这里.

   Locked ownable synchronizers:
	- <0x0000000782139ef8> (a java.util.concurrent.ThreadPoolExecutor$Worker)
复制代码

在 Java 里面凡是使用 java.util.concurrent.locks.AbstractOwnableSynchronizer 或其子类实现同步功能的同步器, 并且被某个线程获得这个同步器锁的, 这个同步器就会显示在这段 Locked ownable synchronizers 里面. Java 里面的2个实现类分别是: ReentrantLock 和 ReentrantReadWriteLock


在 Java 里面, 一个锁(lock)可以是一个内置的(built-in) monitor, 也可以是一个 ownable synchronizer, 也可以是一个与同步器关联的 java.util.concurrent.locks.Condition对象.


尾部特殊线程及其它

在整个 Thread dump 的最下面, 我们可以看到一些信息非常简单的线程, 比如下面这种:

"VM Thread" os_prio=31 cpu=10486.47ms elapsed=314111.17s tid=0x00007fc188405460 nid=0x7c03 runnable  

"GC Thread#0" os_prio=31 cpu=71683.40ms elapsed=314111.18s tid=0x00007fc188507c80 nid=0x4203 runnable  

"GC Thread#1" os_prio=31 cpu=71692.01ms elapsed=314110.89s tid=0x00007fc18800dd80 nid=0xe403 runnable  

"G1 Conc#0" os_prio=31 cpu=295753.03ms elapsed=314111.18s tid=0x00007fc188508ba0 nid=0x4403 runnable  

"G1 Conc#1" os_prio=31 cpu=295775.82ms elapsed=314110.87s tid=0x00007fc136f0aa20 nid=0x1050b runnable  

"VM Periodic Task Thread" os_prio=31 cpu=49667.04ms elapsed=314111.09s tid=0x00007fc188529b50 nid=0x6c03 waiting on condition  

复制代码

这种线程基本都是 JVM 本身的一些线程, 不去处理我们写的业务逻辑, 主要用来维护 JVM 系统本身. 它的元数据跟我们上面介绍的线程元数据一样, 信息更少, 由于很多都是 native 代码编写, 也没有Java 的栈帧信息.

最后, Java Native Interface(JNI)相关的一些信息. 比如:

JNI global refs: 2777, weak refs: 6388
复制代码

死锁信息

如果在做 Thread dump 的时候, 有死锁的存在, Thread dump 里面最后面会标明线程死锁的相关信息, 比如:

Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x0000600003a000d0 (object 0x000000061f613348, a java.io.PrintStream),
  which is held by "Thread-1"

"Thread-1":
  waiting to lock monitor 0x0000600003a185b0 (object 0x000000061f54ddd8, a ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================
"Thread-0":
	at java.io.PrintStream.write(java.base@17.0.2/PrintStream.java:696)
	- waiting to lock <0x000000061f613348> (a java.io.PrintStream)
	at java.io.PrintStream.print(java.base@17.0.2/PrintStream.java:877)
	   ... 省略 ...
"Thread-1":
	at ch.qos.logback.core.rolling.RollingFileAppender.subAppend(RollingFileAppender.java:234)
	- waiting to lock <0x000000061f54ddd8> (a ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy)
        ... 省略 ...
复制代码

我们会在后续的文章中单独介绍死锁, 所以关于死锁的诊断, 发生的可能情况, 这里就不介绍了.

如何获得 Thread dump

上面详细介绍了 Thread dump 里面的一些信息, 那么有哪些方式可以获得线程这些信息呢?

jcmd

jcmd 是 JDK 自带的命令, 通过 jcmd <pid> Thread.print 就可以得到

jstack

jstack 也是 JDK 自带的命令, 通过 jstack -l -e <pid> 也可以获得

其它方式查看线程的信息

jconsole

jconsole 是 JDK 自带的一个桌面工具, 它和 jcmd/jstack 一样, 也在 JDK 的 bin 目录下, 连接上对应的JVM 之后, 它就可以查看线程的相关信息, 还可以检测死锁.

编程方式获取线程信息

通过使用 ThreadMXBean 接口, 能比较容易的在代码中获得Java 线程的相关信息, 大家可以尝试使用如下代码:

public static void main(String[] args) {
    ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean()
        .dumpAllThreads(true, true);
    Arrays.stream(threadInfos).forEach(System.out::println);
}
复制代码

监控 JVM Thread 的一些指标

监控 Java 应用线程的相关指标对于我们观察应用的健康状况很重要, 那么有哪些跟线程相关的指标呢?

  1. 当前活跃的线程数目以及从启动到现在的所有创建的线程数目;
  2. 按照线程的状态(blocked, running, waiting, timed_waiting)来分类的活跃线程数目;
  3. 按照线程是不是 daemon 线程的来分类;
  4. 死锁线程的数目;
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值