Thread
Thread 命令可以查看当前线程信息,查看线程的堆栈.
定位 CPU 问题:thread
直接使用thread命令将默认按照 CPU 增量时间降序排列,只显示第一页数据。
1 | $ thread |
展示当前最忙的前 N 个线程:thread -n
展示当前最忙的前 N 个线程并打印堆栈:
1 | $ thread -n 3 |
- 没有线程 ID,包含
[Internal]表示为 JVM 内部线程,参考dashboard命令的介绍。 cpuUsage为采样间隔时间内线程的 CPU 使用率,与dashboard命令的数据一致。deltaTime为采样间隔时间内线程的增量 CPU 时间,小于 1ms 时被取整显示为 0ms。time线程运行总 CPU 时间。
注意:线程栈为第二采样结束时获取,不能表明采样间隔时间内该线程都是在处理相同的任务。建议间隔时间不要太长,可能间隔时间越大越不准确。 可以根据具体情况尝试指定不同的间隔时间,观察输出结果。
thread –all, 显示所有匹配的线程
显示所有匹配线程信息,有时需要获取全部 JVM 的线程数据进行分析。
显示指定线程的运行堆栈:thread id
1 | $ thread 1 |
线程死锁:thread -b
有时候我们发现应用卡住了, 通常是由于某个线程拿住了某个锁, 并且其他线程都在等待这把锁造成的。 为了排查这类问题,可以通过thread -b命令 一键找出罪魁祸首。
1 | $ thread -b |
注意, 目前只支持找出 synchronized 关键字阻塞住的线程, 如果是
java.util.concurrent.Lock, 目前还不支持。
其他
指定采样时间间隔:thread -i,
thread -i 1000: 统计最近 1000ms 内的线程 CPU 时间。thread -n 3 -i 1000: 列出 1000ms 内最忙的 3 个线程栈
1 | $ thread -n 3 -i 1000 |
查看指定状态的线程:thread –state
1 | [arthas@28114]$ thread --state WAITING |
生成火焰图
通过命令开启采样,观察一段事件后停止,输出一个 html 火焰图文件打开观察即可。1
profiler start
1 | profiler status |
1 | profiler stop |
火焰图的效果大致如下图:

图中的每个色块代表的是一个线程栈帧,色块中是该栈帧加载执行的函数以及一些采样信息。Y 轴代表调用栈的深度,调用从底部开始向上进行,顶部是正在执行的函数,下面是它的父函数,调用栈越深,火焰就越高。X 轴代表抽样数,如果一个函数在 X 轴占据的宽度越大,表示它被抽取到的次数越多,也就意味着它占用的 CPU 时间越长。
需要注意的是,图中的颜色并不重要,只用于区分不同的函数,同时 X 轴的从左到右的顺序也不重要,默认按照字典顺序排序。
我们可以简单举个例子来理解。一般在使用性能分析工具时,我们会选取一个合适的时机启动分析采样,然后等待一段时间后停止采样。假设采样时间为 20 秒,每秒采样 100 次,那么共采样 2000 次,在这 2000 次中,共有 1800 次执行的是 main 方法,其余 200 次执行的是 run 方法,同时 main 方法又调用了很多其他方法,run 方法也是,这就形成了一个从下往上的调用链,在这些调用链中,又同时会收集到每个方法采样的次数,最终形成一张火焰图。
火焰图是可以互动的,点击某个色块,该色块会被放大并占据所有的宽度,这样可以方便地查看某个函数内部的调用链信息。一般观察火焰图,就是看顶层函数占用的宽度,如果有些函数占用的宽度很大,形成了“平顶”,那么就代表该函数可能存在性能问题。当然,有时候直接观察平顶并不能快速定位问题,因为很多时候顶部的函数都是一些底层的库函数,这时候就应该先观察业务代码在火焰图中的宽度,然后再往上观察顶部的库函数来缩小范围。
从上面的火焰图发现TestController类的wl方法占据的 x 轴宽度较长,这是因为我们程序写了一个死循环到消耗了 CPU 。
1 |
|
trace
使用 trace 命令查看耗时
我们写几个不同循环次数的方法,分别代表方法耗时不同,代码如下:
1 | //示例代码,timeCost是个接口 |
部署之后我们使用 trace 来查找最耗时的方法,在此过程中需要知道包名、类名,以及方法的名字。上述代码方法所在包名为 com.cctest.arthas_demo.controller,类名为 StressSceneController,所以我们需要输入如下 trace 命令:
1 | trace |
输完命令后回车,然后 arthas 程序就进入了等待访问状态。这时候访问接口 /timeCost,我们就可以看到被测程序台在疯狂打印日志,等结束之后,arthas 命令窗口便输出了各方法耗时时间,如图 3 所示:
vmtool
查看spring所管理的所有的bean名称
vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBeanDefinitionNames()’
查看具体的某个bean及其属性
vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBean(“testController”)’ -x 3
调用 bean 的方法
vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBean(“testController”).ok(1)’
ognl
ognl 语法:
- #开头变量 在ognl 里面是变量全局的,可以传递 ;
- 字段类型要注意
- Doubble -> 1D
- Long - >1L
- String ->
""
- Class对象-> @xxxClass@class 相当于一个静态的变量
调用 Spring Bean 及对应方法
步骤说明
主要分为三步:
- sc 获取工具类的 classLoaderHash
- ognl 指定工具类的 classLoaderHash 获取对应 Bean
- 根据需要调用的 Bean 对象相关方法进行调用
实战运用
1 | sc -d cn.lovike.function.osier.core.util.SpringBeanUtils |
1 | [arthas@14466]$ sc -d cn.lovike.function.osier.core.util.SpringBeanUtils |
调用 Redis 工具类查看某个 Redis Key 的类型
1 | ognl -c 18b4aac2 -x 3 '@cn.lovike.function.osier.core.util.SpringBeanUtils@getBean("redisUtils").type("osier:sys:dict")' |
返回结果:
ognl -c 2e5d6d97 -x 3 '@com.it.chenyuan.business.utils.SpringUtil@getBean(“MyRedisUtil”).get(“aper_duty_rate:hwzy:1868547696917106690”)’
1 | [HASH] |
调用 Redis 工具类查看某个 Redis Key 是否存在
1 | ognl -c 18b4aac2 -x 3 '@cn.lovike.function.osier.core.util.SpringBeanUtils@getBean("redisUtils").exists("osier:sys:dict")' |
返回结果:1
[true]
单独设置UserController的logger level
1 | ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)' |
排查HTTP请求返回401
请求接口没有权限的时候一般就返回401 Unauthorized。
401通常是被权限管理的Filter拦截了,那么到底是哪个Filter处理了这个请求,返回了401?
跟踪所有的Filter函数
开始trace:
1 | trace javax.servlet.Filter * |
可以在调用树的最深层,找到AdminFilterConfig$AdminFilter返回了401
1 | +---[3.806273ms] javax.servlet.FilterChain:doFilter() |