前言

在项目中遇到一个问题,我们服务提供给外部的一个接口 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。

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

SectionExample解释
线程名字main 和 Reference Handler可读的线程名字,这个名字可以通过 Thread 方法 setName 设定
线程 ID#1每一个 Thread 对象的唯一 ID,这个 ID 是自动生成的,从 1 开始,通过 getId 方法获得
是否守护线程daemon这个标签用来标记线程是否是守护线程,如果是会有标记,如果不是这没有
优先级prio=10Java 线程的优先级,可以通过 setPriority 方法设置
OS 线程的优先级os_prio
CPU 时间cpu=94.43ms线程获得 CPU 的时间
elapsedelapsed=509136.51s线程启动后经过的 wall clock time
AddresstidJava 线程的地址,这个地址表示的是 JNI native Thread Object 的指针地址
OS 线程 IDnidThe 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


文章标题:JStack 使用介绍
文章作者:cylong
文章链接:https://0skyu.cn/p/63ea.html
有问题或者建议欢迎在下方评论。欢迎转载、引用,但希望标明出处,感激不尽(●’◡’●)