0%

JStack 使用介绍


前言

在项目中遇到一个问题,我们服务提供给外部的一个接口 queryXXX 一直返回 429 错误(Too Many Requests),接口没有返回值,而且服务越用越卡,要重启一下才能恢复。于是马上就想到是不是因为这个接口产生了死循环,导致接口无法正确返回,同时导致后台 CPU 和内存占用飙升,顺着这个思路定位下去,确实顺利的找到的问题所在。

定位思路

  1. 执行 free -m/free -h 查看服务的后台 CPU 和内存占用,发现服务占用的内存和 CPU 过高。
  2. 执行 jps/ps/top 命令找到 CPU 和内存占用高的进程 ID(32033)。
  3. 执行 top -H -p 32033,找到 %CPU 和 %MEM 占用高的 PID(60958),转换成16进制 ee1e。
  4. 执行 jstack -l 32033 > stack.txt,打印调用栈信息。
  5. 找到 stack.txt 日志里面的 nid=0xee1e(注意第三步的是十进制,日志里的是十六进制)对应接口服务,分析调用栈,发现 queryXXX 接口的状态一直是 RUNNING 状态,卡死。
  6. 定位后发现代码中使用了流 API 的 parallelStream 导致的问题,原因是 parallelStream 是并行操作,我们这边使用了 HashMap,HashMap 是非线程安全的,并发插入数据在 resize() 方法中产生循环链表导致死循环(JDK8 已经解决),导致 queryXXX 接口的状态一直是 RUNNING,无返回值,CPU 和内存飙升。调用服务方在没有接受到返回的时候不断请求这个服务,于是产生了 429 错误。
  7. 我们这边 HashMap 是局部变量,解决方法是将 parallelStream 并行流修改为 stream 串行流。如果此 HashMap 是那种全局变量,涉及并发操作,则可以改成使用 ConcurrentHashMap。

关于 HashMap 的介绍可以参考:

浅谈 HashMap | 笑话人生

使用介绍

JStack 是 java 自带的工具,在 jdk\bin\jstack.exe 位置。以下是 Windows 的示范,在 Linux 系统上功能更多。

1
2
3
4
5
6
7
8
9
PS C:\Program Files\Java\jdk-11.0.2\bin> .\jstack
Usage:
jstack [-l][-e] <pid>
(to connect to running process)

Options:
-l long listing. Prints additional information about locks
-e extended listing. Prints additional information about threads
-? -h --help -help to print this help message

一般常用的是以下的命令:

1
2
jstack -l [PID]
jstack -F [PID]
  • -l 选项会打印额外的信息,比如说锁信息。
  • 当进程挂起(hung)时,上面的命令可能没有响应,这时需要使用 -F 参数来强制执行 thread dump。

接下来我们就可以分析打印的堆栈信息进行分析,比如我上面列举的那个问题:

Section Example 解释
线程名字 main 和 Reference Handler 可读的线程名字,这个名字可以通过 Thread 方法 setName 设定
线程 ID #1 每一个 Thread 对象的唯一 ID,这个 ID 是自动生成的,从 1 开始,通过 getId 方法获得
是否守护线程 daemon 这个标签用来标记线程是否是守护线程,如果是会有标记,如果不是这没有
优先级 prio=10 Java 线程的优先级,可以通过 setPriority 方法设置
OS 线程的优先级 os_prio
CPU 时间 cpu=94.43ms 线程获得 CPU 的时间
elapsed elapsed=509136.51s 线程启动后经过的 wall clock time
Address tid Java 线程的地址,这个地址表示的是 JNI native Thread Object 的指针地址
OS 线程 ID nid The unique ID of the OS thread to which the Java Thread is mapped
线程状态 Running 线程当前状态,线程状态下面就是线程的堆栈信息

线程的运行状态:

  • New: 线程对象创建,不可执行。
  • Runnable: 调用 thread.start() 进入 runnable,获得 CPU 时间即可执行。
  • Running: 执行状态。
  • Waiting: thread.join() 或调用锁对象 wait() 进入该状态,当前线程会保持该状态直到其他线程发送通知到该对象。
  • Timed_Waiting:执行 Thread.sleep(long)、thread.join(long) 或 obj.wait(long) 等就会进该状态,与 Waiting 的区别在于 Timed_Waiting 的等待有时间限制;
  • Blocked: 等待锁,进入同步方法,同步代码块,如果没有获取到锁会进入该状态。该线程尝试进入一个被其他线程占用的 synchronized 块,当前线程直到锁被释放之前一直都是 blocked 状态。
  • Dead:执行结束,或者抛出了未捕获的异常之后。
  • Deadlock: 死锁。
  • Waiting on condition:等待某个资源或条件发生来唤醒自己。
  • Waiting on monitor entry:在等待获取锁。
  • terminated: 线程已经结束 run() 并且通知其他线程 joining。

此文开头解决的问题,由于是公司项目,不方便贴上定位的过程日志和代码,所以就先记录下定位的思路和基本概念。

感谢

java命令–jstack 工具 | milkty
每天学习一个命令:jstack 打印 Java 进程堆栈信息 | Ein Verne
Java 性能调优学习笔记
Using Thread Dumps | Oracle


分享精彩,留下足迹

欢迎关注我的其它发布渠道