前言
在项目中遇到一个问题,我们服务提供给外部的一个接口 queryXXX 一直返回 429 错误(Too Many Requests),接口没有返回值,而且服务越用越卡,要重启一下才能恢复。于是马上就想到是不是因为这个接口产生了死循环,导致接口无法正确返回,同时导致后台 CPU 和内存占用飙升,顺着这个思路定位下去,确实顺利的找到的问题所在。
定位思路
- 执行
free -m/free -h
查看服务的后台 CPU 和内存占用,发现服务占用的内存和 CPU 过高。 - 执行
jps/ps/top
命令找到 CPU 和内存占用高的进程 ID(32033)。 - 执行
top -H -p 32033
,找到 %CPU 和 %MEM 占用高的 PID(60958),转换成16进制 ee1e。 - 执行
jstack -l 32033 > stack.txt
,打印调用栈信息。 - 找到 stack.txt 日志里面的
nid=0xee1e
(注意第三步的是十进制,日志里的是十六进制)对应接口服务,分析调用栈,发现 queryXXX 接口的状态一直是 RUNNING 状态,卡死。 - 定位后发现代码中使用了流 API 的 parallelStream 导致的问题,原因是 parallelStream 是并行操作,我们这边使用了 HashMap,HashMap 是非线程安全的,并发插入数据在 resize() 方法中产生循环链表导致死循环(JDK8 已经解决),导致 queryXXX 接口的状态一直是 RUNNING,无返回值,CPU 和内存飙升。调用服务方在没有接受到返回的时候不断请求这个服务,于是产生了 429 错误。
- 我们这边 HashMap 是局部变量,解决方法是将 parallelStream 并行流修改为 stream 串行流。如果此 HashMap 是那种全局变量,涉及并发操作,则可以改成使用 ConcurrentHashMap。
使用介绍
JStack 是 java 自带的工具,在 jdkbinjstack.exe 位置。以下是 Windows 的示范,在 Linux 系统上功能更多。
代码语言:javascript复制PS C:Program FilesJavajdk-11.0.2bin> .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
一般常用的是以下的命令:
代码语言:javascript复制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。
此文开头解决的问题,由于是公司项目,不方便贴上定位的过程日志和代码,所以就先记录下定位的思路和基本概念。