使用堆栈信息排查系统性能问题,多线程程序调优(因为这类问题往往都不会输出日志或日志输出定位代码排查的范围太大),堆栈信息对这类问题定位速度快而精准。
查询堆栈信息全文涉及到的指令
线程状态相关 常见到的线程的状态(java.lang.Thread.State)
状态 | 描述 |
RUNNABLE | 正在运行中的线程 |
LOCKED | 线程进行时受到阻塞,可能在等待释放锁,程序中可能有方法修饰了 synchronize (同步) |
TIMED_WAITING | 延时线程,到了指定的时间的时候会被触发 |
WAITING | 线程未设置时间,只能通过interup或者notifyAll激活,通常用于不定时间不定的条件限制等待,需注意使用.wait()时需要在有同步锁的状态下否则会报错 |
指令相关
系统 | 指令 | 使用方法 | 描述 |
Linux | jstack | jstack <进程ID> | 输出Java进程里的线程堆栈信息 |
jstack <进程ID> | grep -A<筛选内容后输出几行的数字> <筛选内容> | Java进程里的线程堆栈 信息筛选输出以及输出包括筛选信息后几行的内容 | ||
jstack <进程ID> -> <随意指定的文件名> | 输出Java进程里的线程堆栈 信息到文件中(若文件不存在则新生成文件并写入) | ||
ps | ps -T -p | 查看进程下创建的所有线程列表信息 | |
ps -aux|grep "筛选内容" | 筛选内容 | ||
ps -ef|grep "筛选内容" | 筛选内容 | ||
Windows | netstat | netstat -ano|findstr "PID进程号" | 根据PID查询进程占用端口 |
Linux 系统下堆栈信息排查教程(以高CPU占用线程为例)
1.首先找到托管项目的Tomcat所在PID(进程ID)(ps -aux|grep “筛选内容” 或 ps -ef|grep “筛选内容”),如果项目占用内存或CPU过大可直接通过 top 指令查看并获取 PID
2.通过找到的PID来查看其进程下的所有线程运行情况(top -T -p )
PID=线程ID (该指令查出来的PID虽然是进程ID的名字但是实际上确实是线程ID)
TIME=线程执行时间 (可排查执行时间长的线程执行情况)
%CPU=占用CPU比例 (可排查高CPU占用的线程执行情况)
%MEM=占用内存比例 (可排查高内存占用的线程执行情况)
3.找到高 CPU 占用的线程,使用 printf “%x\n” <线程ID> 来得到该线程在堆栈信息中的十六进制数,然后使用 (jstack |grep -A20 0x<线程十六进制内容> --color) 指令查询并筛选线程堆栈信息 nid 为 tid 转成十六进制数之后的前缀加上0x的内容,并加上筛选内容后20行堆栈信息之后用 -color 给筛选内容染色输出显示
PS:每一个线程TID对应一个十六进制转换后前缀加上0x的NID
4.最后就能根据堆栈内容定位到问题代码所在地然后进行修改
Win 系统下堆栈信息排查教程(以高CPU占用线程为例)
Process Explorer 官方下载地址:https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer
Process Explorer 网盘下载地址:https://pan.baidu.com/s/1NxSvzqTL8yWKeiEqLeckGA
1.若找不到对应的是不是自己项目的进程则打开Windows的CMD控制台输入 netstat -ano|findstr “端口号” 来搜索端口对应的进程号PID,其他的端口号都为程序启动时涉及到使用的其他端口号(例如Redis[6379] MonogDb[27017] Mysql[6379]),无需理会,如果找到了Tomcat占用端口正好对应项目启动时设置的端口号,则证明该进程PID是我们要调试的堆栈信息的进程
2.根据Tomcat占用的端口找到项目的PID进程ID之后打开 Process Explorer 根据PID找到进程双击进入查看进程的线程信息,并根据高CPU占用率的线程信息拿到线程TID
3.根据找到的问题线程的线程ID TID 打开计算机切换到程序员模式点击十进制模式然后输入数字之后就能看到该数字转换成十六进制之后的值
4.用之前得到的进程ID 使用 jstack <进程ID> 指令输出进程的线程堆栈信息并且用计算后的十六进制的值前缀加上0x之后对线程堆栈信息进行筛选(由于是Windows系统我直接用的 Ctrl + F 筛选 0x3894)
5.最后就能根据堆栈内容定位到问题代码所在地然后进行修改
附加内容(其中3和4试过了,确实会如此)
释放锁情景
1.执行完同步代码块。
2.在执行同步代码块的过程中,遇到异常而导致线程终止。
3.在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。
除了以上情况外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁
不释放锁情景
4.在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
5.在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁
6.在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃。
Demo下载: https://pan.baidu.com/s/1tEZyWppyA_dVpyBFA8i97w