jstack性能问题定位案例分析详解

在Java应用的性能测试中,很多性能问题可以通过观察线程堆栈来发现,Jstack是JVM自带dump线程堆栈的工具,很轻量易用,并且执行时不会对性能造成很大的影响。灵活的使用jstack可以发现很多隐秘的性能问题,是定位问题不可多得的好帮手。

1

什么是线程堆栈

线程堆栈也称作线程调用堆栈。Java线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况等信息,从线程堆栈中可以得到以下信息:

  1. 线程的名字,ID,线程的数量等;

  2. 线程的运行状态,锁的状态(锁被那个线程持有,哪个线程在等待锁等);

  3. 函数间的调用关系,包括完整类名,所执行的方法,源代码的行数等;

可以通过Jstack获取应用运行时的线程堆栈,可以通过如下方式获取线程堆栈:

jstack pid>>jstack.log

对于Java应用而言,一下常见的几个性能问题都可以从线程堆栈入手定位:

  • 系统挂起无响应

  • 系统CPU较高

  • 系统运行的响应时间长

  • 线程死锁等

2

线程的运行状态

想知道线程是在卖力工作还是偷懒休息,这就需要关注线程的运行状态,常用到的几个线程状态有:RUNNABLE,BLOCKED,WAITING,TIMED_WAITING。

RUNNABLE

从虚拟机的角度看,RUNNABLE状态代表线程正处于运行状态。一般情况下处于运行状态线程是会消耗CPU的,但不是所有的RUNNABLE都会消耗CPU,比如线程进行网络IO时,这时线程状态是挂起的,但由于挂起发生在本地代码,虚拟机并不感知,所以不会像显示调用Java的sleep()或者wait()等方法进入WAITING状态,只有等数据到来时才消耗一点CPU.

TIMED_WAITING/WATING

这两种状态表示线程被挂起,等待被唤醒,当设置超时时间时状态为TIMED_WAITING,如果是未设置超时时间,这时的状态为WATING,必须等待lock.notify()或lock.notifyAll()或接收到interrupt信号才能退出等待状态,TIMED_WAITING/WATING下还需要关注下面几个线程状态:

  • waiting on condition:说明线程等待另一个条件的发生,来把自己唤醒;

  • on object monitor: 说明该线程正在执行obj.wait()方法,放弃了 Monitor,进入 “Wait Set”队列;

BLOCKED

此时的线程处于阻塞状态,一般是在等待进入一个临界区“waiting for monitor entry”,这种状态是需要重点关注的

哪些线程状态占用CPU?

处于TIMED_WAITING、WATING、BLOCKED状态的线程是不消耗CPU的,而处于RUNNABLE状态的线程要结合当前线程代码的性质判断是否消耗CPU:

  • 纯java运算代码,并且未被挂起,是消耗CPU的;

  • 网络IO操作,在等待数据时是不消耗CPU的;

3

使用jstack定位问题

案例一:tomcat应用无法启动

问题现象:环境搭建时,部署应用后tomcat无法启动,查看日志并无报错现象,直观感觉tomcat启动时好像卡在了哪里,所以我们希望看到tomcat启动时究竟发生了什么,导致启动无法完成,这时线程堆栈中的函数调用关系也许可以帮上忙,jstack得到对应tomcat应用的线程堆栈,如下:

问题分析:首先关注线程状态,是处于WATING(on object monitor),这时线程执行了Object.wait(),处于挂起状态,在等待被唤醒,而且这里并没有设置超时时间,所以只要线程没被唤醒,tomcat会一直等下去。但tomcat在等什么呢,查看函数调用信息可以看到“com.besttest.andashu.*****.CuratorSupport.initZK”,这个函数是被测项目启动时需要初始化zookeeper,应用启动就是卡在了这里。知道问题所在就好办, 查看被测项目的配置,发现zookeeper的ip用的是私有ip,与应用不通,更改成机房ip后问题解决。

案例二:数据库连接池不够用导致响应时间久

问题现象:在测试一个场景时,发现响应时间很长,日志也无报错现象,根据调用链逐级定位,发现80%的时间都是消耗在DAO层的方法上,这时首先考虑的是sql会不会有问题?于是找DBA同学帮忙抓sql看下,但DBA同学反映sql执行很快,执行计划也没有问题,那问题出现在哪里呢,找不到原因就看下线程堆栈,系统在dao层方法后做了什么?jstack线程堆栈如下:

问题分析:先关注线程状态,发现堆栈信息里大量的dubbo线程处于TIMED_WAITING状态,从“waiting on condition”可以看出系统在等待一个条件发生,这时的线程处于sleep状态,一般会有超时时间唤醒,一般出现TIMED_WAITING很正常,一些等待IO都会出现这种状态,但是大量的TIMED_WAITING就要找原因了,观察线程堆栈发现处于TIMED_WAITING状态的线程都在等待druid获取连接池的连接,这种现象很想连接池不够用了,于是增加数据库连接池的连接数,TPS直接提升了3倍。

案例三:线程阻塞导致响应变慢

问题现象:同样是在测试场景时发现响应时间变慢,并且响应时间的毛刺现象比较严重,依次排查系统可能的瓶颈点没有明显收获,这时jstack又排上用场了,先看线程堆栈:

问题分析:可以看到线程是处于BLOCKED状态的,这种状态我们需要重点关注,这时的线程是被阻塞的,进一步查看发现几乎所有的dubbo线程都处于block状态,都在“waiting to lock <0x000000078c312718>”,这个<0x000000078c312718>又是个什么鬼?

通过排查发现这个锁是log4j拿到的,同时阻塞了其他线程通过log4j打日志,Google类似问题才知道是log4j的一个bug,可以通过升级log4j版本或者精简日志避免,知道原因后经过相应的处理,性能得到大幅度提升。

案例四:系统CPU占用率高

问题现象:CPU占用率高的问题很常见,首先我们要确定是不是usr%较高,如果是我们就可以借助jstack来看看究竟是什么业务占用了这么高的CPU。

问题分析:首先top找出CPU占用率较高的进程PID

可以看出PID为51334的占用CPU最高,运行命令

top-p 51334-H

等到该进程所有线程的统计情况:

这里假设PID为51402的线程很高,把PID转换成十六进制0xc8ca,这个既是jstack堆栈信息中的线程nid:

通过观察线程堆栈就能得到是哪个方法的调用导致CPU占用较高,记得要多jstack几次。

总 结

通过线程堆栈还可以分析出死锁,死循环等性能问题,平时在定位性能问题时多jstack几次,可能会有意想不到的收获哦。

猜你喜欢

转载自blog.csdn.net/mlljava1111/article/details/81170787