使用 Arthas(阿尔萨斯)排查 Java 程序线上问题
Arthas(阿尔萨斯)是阿里巴巴开源的一款用于线上诊断 Java 程序的工具,通过全局视角实时查看应用程序加载、内存、垃圾回收和线程的状态等信息,并能在不修改程序代码和不中断程序运行的情况下,对程序运行问题进行排查,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载等信息,能够大大提升线上问题排查的效率。

前言:
通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用本地 IDE 远程调试(部分情况下可通过本地 IDE 远程 Debug 生产服务),并且,大部分情况下并不允许在生产环境中调试,因为它可能会影响业务,甚至中断程序。
一般情况下,若遇到生产问题,则会在测试环境中尝试复现生产问题,但某些问题在不同环境中不能轻易复现甚至不能复现。
故,若你想在生产环境中排查问题,尤其是 JVM 问题,那么不妨试试 Arthas,无需重启程序,无需修改代码,无需中断业务,如菲奥娜的被动 决斗之舞 般,可轻松发现正在运行的程序所漏出破绽。
1 使用场景
- 1、这个类是从哪个 jar 包加载的?为什么会报各种类相关的
Exception? - 2、我改的代码为什么没有执行到?难道是我没
commit或是部署没生效? - 3、遇到问题无法线上 debug,难道只能通过添加日志再重新发布吗?
- 4、线上遇到某个用户的数据处理有问题,但线上无法 debug,线下无法重现!
- 5、是否有一个全局视角来查看系统的运行状态?
- 6、有什么办法可以监控到 JVM 的实时运行状态?
- 7、怎么快速定位应用的热点,生成火焰图?
- 8、怎样直接从 JVM 内查找某个类的实例?
- 9、面试时老被问到生产问题怎么排查?JVM 如何调优?甚至刚毕业就要求三年经验!
提示
以上所有问题,通过 Arthas 都可以解决!
2 下载安装
2.1 下载
2.1.1 从 maven 仓库下载
下载地址为 https://arthas.aliyun.com/download/latest_version?mirror=aliyun,下载文件为 zip 压缩包,下载后解压即可。
2.1.2 从 github relese 下载
下载地址为 https://github.com/alibaba/arthas/releases。
2.1.3 命令行下载
执行以下命令下载:
wget https://alibaba.github.io/arthas/arthas-boot.jar
# 或
# 推荐以此方式安装
curl -O https://arthas.aliyun.com/arthas-boot.jar
2.2 启动
2.2.1 用 as.sh 启动
解压后,执行文件夹内的 as.sh 脚本文件启动,命令如下:
./as.sh
# 或
./as.sh -h # -h 可打印帮助信息
2.2.2 用 arthsa-boot 启动
arthas 的启动实际上是执行 arthas-boot.jar jar 包,解压后,文件夹内有 arthas-boot.jar,直接使用以下命令启动:
java -jar arthas-boot.jar
3 快速入门
3.1 启动 arthas
使用 java -jar arthas-boot.jar 命令启动 arthas 程序后会输出以下内容,其中 1 2 3 4 5 编号所标识的 java 进程则是当前可监测的正在运行的 java 程序。

⚠️注意
请注意,在使用
java -jar arthas-boot.jar命令启动arthas时,当前用户需要和目标进程的启动用户保持一致,否则可能会attach(附加)失败。若attach不上目标进程,则可查看~/logs/arthas/目录下的日志。
编号 4 所标识的 UserCenterApplication 程序则是此次的 demo,输入 4 再回车后,arthas 便会附加到目标进程上,并输出相关日志:
3.2 查看进程信息
可通过 dashboard 命令查看当前进程对应控制面板,即展示当前进程的实时信息,包括线程的名称、运行状态、CPU 占用等信息,内存信息,运行时环境等信息。按 ctrl + c 可中断执行。

关于 dashboard 命令的详细信息将在下文进行说明。
3.3 查看线程堆栈
通过 thread 命令加线程 ID 即可查看某个线程的当前堆栈信息。
3.4 反编译类
通过 jad 命令加全类名,即可查看某个已加载类的反编译信息。

3.5 退出 arthas
如果只退出针对当前进程的连接,则使用 exit 或 quit 即可,执行后 arthas 依旧会 attach 到目标进程上,端口会保持开放,下次连接时可直接连接上。若想完全退出 arthas,则执行 stop 命令即可。

4 命令列表
4.1 基础命令
base64:base64编码转换,和linux中的base64命令类似。cat:打印文件内容,和linux中的cat命令类似。cls:清空当前屏幕区域。echo:打印参数,和linux中的echo命令类似。grep:匹配查找,和linux中的grep命令类似。help:查看命令帮助信息。history:打印命令历史。keymap:arthas快捷键列表及自定义快捷键。pwd:返回当前的工作目录,和linux中的pwd命令类似。reset:重置增强类,将被arthas增强过的类全部还原,arthas服务类关闭时会重置所有增强过的类。session:查看当前会话的信息。stop:关闭arthas服务端,所有arthas客户端全部退出。tee:复制标准输入到标准输出到指定的文件,和linux中的tee命令类似。version:输出附加到当前进程的arthas的版本号。
4.2 jvm 相关
dashboard:当前进程的实时信息面板。getstatic:查看类的静态属性。heapdump:dump java heap,类似jmap命令的heap dump功能,用于生成某一时刻的对内存快照。jvm:查看当前 jvm 信息。logger:查看和修改logger。mbean:查看Mbean的信息。memory:查看 jvm 的内存信息。ognl:执行ognl表达式。perfcounter:查看当前 jvm 的Perf Counter信息。sysenv:查看 jvm 的环境变量。sysprop:查看和修改 jvm 的系统属性。thread:查看当前 jvm 中某个线程的堆栈信息。vmoption:查看和修改 jvm 里诊断相关的option。vmtool:从 jvm 里查询对象,执行forceGc。
4.3 class/classloader 相关
classloader:查看classloader的继承树,urls,类加载信息,使用classloader去getResource。dump:dump已加载类的byte code到指定目录。jad:反编译指定已加载类的源码。mc:内存编译器,内存编译.java文件为.class文件。redefine:加载外部的.class文件,redefine到 jvm 里。retransform:加载外部的.class文件,retransform到 jvm 里。sc:查看 jvm 已加载的类的信息。sm:查看 jvm 已加载的类的方法信息。
4.4 monitor/watch/trace 相关
⚠️注意
请注意,这些命令都是通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行
stop或将增强过的类执行reset命令。
monitor:方法执行监控。stack:输出当前方法被调用的调用路径。trace:方法内部调用路径,并输出方法路径上的每个节点的执行耗时。tt:方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同时间下的调用进行观测。watch:方法执行数据观测,可观测 返回值、抛出异常、入参等。
4.5 profiler/火焰图
profiler:使用async-profiler对应用采样,生成火焰图。关于async-profiler更多的信息,可查看 https://github.com/jvm-profiling-tools/async-profiler。jfr:动态开启关闭JFR记录。
4.6 后台异步任务
当线上出现偶发问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,则可以使用此类命令。
- 使用
>将结果重写向到日志文件,使用&指定命令是后台运行,session断开不影响任务执行(生命周期默认为 1 天)。 jobs:列出所有job。kill:强制终止任务。fg:将暂停的任务拉到前台执行。bg:将暂停的任务放到后台执行。
5 常用命令
5.1 dashboard
查看当前进程对应控制面板,即展示当前进程的实时信息,包括线程的名称、运行状态、cpu 占用等信息,内存信息,运行时环境等信息。按 ctrl + c 可中断执行。
参数说明:
[i:]:刷新实时数据的时间间隔(ms),默认 5000ms。[n:]:刷新实时数据的次数。
数据说明:
ID:java 级别的线程 ID,注意这个 ID 不能和jstack中的nativeID一一对应。NAME:线程名。GROUP:线程组。PRIORITY:线程优先级,1 ~ 10 之间的数字,越大表示优先级越高。STATE:线程的状态。CPU%:线程的 cpu 使用率。比如采样间隔 1000ms,某个线程的增强 cpu 时间为 100ms,则 cpu 使用率 =100/1000=10%。DELTA_TIME:上次采样之后线程运行增量 cpu 时间,数据格式为 秒。TIME:线程运行总 cpu 时间,数据格式为 分:秒。INTERRUPTED:线程当前的中断状态。DAEMON:是否为daemon线程。
Java 8 之后支持获取 JVM 内部线程 cpu 时间。JVM 内部线程如:
- JIT 编译线程:如
C1 CompilerThread0,C2 CompilerThread0。 - GC 线程:如
GC Thread0,G1 Young RemSet Sampling。 - 其它内部线程:如
VM Periodic Task Thread,VM Thread,Service Thread。
截图展示:

5.2 memory
查看 JVM 内存信息,具体字段信息可参考:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryMXBean.html#getHeapMemoryUsage()。
- https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryMXBean.html#getHeapMemoryUsage()
截图展示:

5.3 thread
查看当前进程中某个线程的堆栈信息。如 thread 45,其中 45 为线程 ID。
参数说明:
id:线程 id。[n:]:指定最忙的前 N 个线程并打印堆栈。[b]:找出当前阻塞其它线程的线程。[i<value>]:指定 cpu 使用率统计的采样间隔,单位为毫秒,默认值为 200。[--all]:显示所有匹配的线程。
提示
这里的 cpu 使用率与 linux 命令
top -H -p <pid>的线程%CPU类似,一段采样间隔时间内,当前 jvm 里各个线程的增量 cpu 时间与采样间隔时间的比例。
工作原理:
- 首先第一次采样,获取所有线程 cpu 时间,调用的是
java.lang.management.ThreadMXBean#getThreadCpuTime()及sun.management.HotspotThreadMBean.getInternalThreadCpuTimes()接口。 - 然后睡眠等待一个间隔时间(默认为 200ms,可通过
-i指定间隔时间)。 - 再次第二次采样,获取所有线程的 cpu 时间,对比两次采样数据,计算出每个线程的增量 cpu 时间。
- 线程 cpu 使用率 =
线程增量 cpu 时间 / 采样间隔时间 * 100%。
⚠️注意
这个统计也会产生一定的开销(JDK 这个借口本身开销比较大),因此会看到 as 的线程占用一定的百分比,为了降低统计自身的开销所带来的影响,建议拉长采样间隔时间,比如 5000ms。
截图展示:
5.4 vmtool
利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能。JVMTI 文档见 https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html。
参数说明:
[--action]:指定操作名。[--className]:指定类名。[--classLoaderClass]:指定类加载器名。[-c]:指定classloader的hash值。[--limit]:限制返回值数量,以避免获取超大数据时对 jvm 造成压力,默认值为 10。
查询对象截图展示:

其它功能:
-
强制 GC:
# 可结合 vmoption 命令动态打开 PrintGC 开关 vmtool --action forceGC -
interrupt 指定线程:
# thread id 通过 -t 参数指定,可使用 thread 命令获取 vmtool --action interruptThread -t 1 -
glibc 释放空闲内存:
# Linux man page:malloc_trim 查看 https://man7.org/linux/man-pages/man3/malloc_trim.3.html vmtool --action mallocTrim -
glibc 内存状态:
# 内存状态会输出到应用的 stder。 # Linux man page:malloc_stats 查看 https://man7.org/linux/man-pages/man3/malloc_stats.3.html vmtool --action mallocStats
5.5 jad
查看已加载到 jvm 中的类的源码的反编译信息。通过查看反编译后的 java 代码,便于分析代码逻辑,以及确认发布后的代码是否为你修改的代码。
- 反编译后的代码是语法高亮的,便于阅读。
- 反编译后的代码可能会存在语法错误,但不影响阅读理解。
参数说明:
class-pattern:类名表达式匹配。[c:]:类所属ClassLoader的hashCode。[classLoaderClass:]:指定执行表达式的ClassLoader的 class name。[E]:开启正则表达式匹配,默认为通配符匹配。[--lineNumber]:是否显示行号,默认为 true。[--source-only]:默认情况下,反编译结果里会带有ClassLoader信息,可通过该命令纸打印源代码。方便和mc/retransform命令结合使用。[>]:将结果输出到目标文件,如> Test.java。
截图展示:

5.6 retransform
加载外部的 .class 文件,retransform jvm 已加载的类,即热更新类,这个功能非常有用!
热更新的操作步骤是什么呢?首先修改目标类,然后将目标类转换成 .class 文件,最后再热更新,这一系列操作都可以通过相关命令实现。那如果我们要修改的内容很少,直接 vim 就可以修改,那么它的具体操作可以是这样:
-
1、使用
jad命令反编译输出目标类的源码。jad --source-only org.xgllhz.usercenter.blog.web.controller.WebBlogController > WebBlogController.java -
2、使用
vim命令直接修改。 -
3、使用
mc将.java文件编译成字节码文件.class。mc WebBlogController.java -d ./ -
3、使用
retransform命令热更新编译后的字节码文件。retransform WebBlogController.class
提示
若多次执行
retransform加载同一个 class 文件,则会有多条retransform entry。
针对 retransform entry 的操作:
-
查看
retransform entry:retransform -l -
删除指定
retransform entry:# 1 为 id retransform -d 1 -
删除所有 retransform entry:
retransform --deteleAll
消除 retransform 的影响:若对某个类执行 retransform 之后,想消除影响,则需要:
- 删除这个类对应的
retransform entry。 - 重新出发
retransform。
提示
若不清楚掉所有的
retransform entry,并重新出发retransform,则stop时,retransform过的类仍然生效。
retransform 的限制:
- 不允许增加新的
field/method。 - 正在运行的函数,没有退出则不会生效。
⚠️注意
redefine命令与retransform命令功能类似,但建议使用后者。
5.7 sc 与 sm
search class 的简写,查看所有已加载到 jvm 中的 class 的信息。
参数说明:
class-pattern:类名表达式匹配。method-pattern:方法名表达式匹配。[d]:输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的classloader等详细信息,若一个类被多个classloader加载,则会出现多次。[E]:开启正则表达式匹配,默认为通配符匹配。[f]:输出当前类的成员变量信息(需要配合参数-d一起使用)。[x:]:指定输出静态变量时属性的遍历深度,默认值为 0,即直接使用toString输出。[c:]:指定class的classloader的hashCode。[classLoaderClass:]:指定执行表达式的ClassLoader的 class name。[n:]:具有详细信息的匹配类的最大数量,默认值为 100.[cs <arg>]:指定 class 的ClassLoader#toString()的返回值。
截图展示:

⚠️注意
sm与sc命令大同小异,需要注意的是sm命令只能看到由当前类所定义的方法,父类的则看不到。
5.8 stack
查看当前方法被调用的调用路径。
参数说明:
class-pattern:类名表达式匹配。method-pattern:方法名表达式匹配。condition-express:条件表达式。[E]:开启正则表达式匹配,默认为通配符匹配。[n:]:执行次数限制。[m <arg>]:指定 class 最大匹配数量,默认值为 50。
⚠️注意
这里重点要说明的是观察表达式,观察表达式的构成主要由
ognl表达式组成,所以你可以这样写{params, returnObj},只要是一个合法的ognl表达式,都能被正常支持。观察的维度也比较多,主要体现在参数
advice的数据结构上。Advice参数最主要是封装了通知节点的所有信息。
截图展示:

5.9 trace
查看方法内部调用路径,并输出方法路径上每个节点的耗时。该命令会主动搜索 class-pattern / method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
参数说明:
class-pattern:类名表达式匹配。method-pattern:方法名表达式匹配。condition-express:条件表达式。[E]:开启正则表达式匹配,默认为通配符匹配。[n:]:命令执行次数,默认值为 100.#cost:方法执行耗时。[m <arg>]:指定 class 最大匹配数量,默认值为 50。
⚠️注意
重点说明条件表达式,条件表达式主要由
ognl表达式组成,所以你可以这样写params[0]<0,只要是合法的ognl表达式,都能被正常支持。很多时候我们可能只想看到某个方法的
rt大于某个时间之后的trace结果,巧的是arthas可以按照方法执行耗时来过滤了,比如你可以这样写trace *StringUtils isBlank '#cost>100',表示当该方法的执行超过 100ms 时才会输出trace的结果。不得不说,这个功能太屌了。
提示
stack/trace/watch这三个命令都支持#cost参数。
截图展示:

其中每个方法前的结果表示该方法在此次调用链路中耗时所占百分比和耗时,方法后面的 #num(如 #62)表示该方法在源文件的第 num 行被调用。
5.10 watch
可观测指定函数的调用情况,如入参、返回值、抛出异常等信息,且支持 ognl 表达式。
⚠️注意
下面有一个与
watch命令功能相似的命令叫tt,二者的区别是watch虽然很方便灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,且很多时候我们并不知道问题出自何方,只能靠蛛丝马迹进行猜测,但是如果能记录下当时方法调用的所有入参和返回值、抛出的异常等信息,那将对整个问题的思考和排查有很大帮助,没错儿,tt命令的功能就是这个。很明显,当我想到写这个醒目的⚠️时,我心思就早已飘到
tt去了。
5.11 tt
方法执行数据的时空隧道,记录下指定方法每次调用的入参、返回值和异常等信息,并能对这些不同时间下的调用进行观测。
‼️警告
tt命令的实现原理是:将函数的入参/返回值等信息,保存到一个Map<Integer, TimeFragment>中,该map的默认容量大小是 100。
tt相关功能在使用完之后,需要手动释放内存,否则长时间可能会导致 OOM,且即使退出athas也不会自动清除tt的缓存map。
参数说明:
[-t]:指定要记录的目标方法。[-n:]:指定需要记录的次数,当达到记录次数时arthas会主动中断 tt 命令的记录过程,避免人工操作无法停止的情况。为什么会有这个参数呢?你是否经历过这样的场景:当你打开日志想要观察某个方法的日志时,结果好不容易定位日志后瞬间就被后续的日志刷到十万八千里外了,没错儿,这个参数就是干这个的。[-m:]:指定 class 匹配的最大数量,防止匹配到的 class 数量太多导致 jvm 挂起,默认值为 50。[-s]:搜索表达式,如-s 'method.name=="listBlogPage"'。[-i]:查看某个时间片的详细信息,值为该时间片的INDEX编号值。[-p]:重新调用一次。[-w]:观察表达式。[-d:]:后加-i,清除指定索引的tt记录。[--delete-all]:清除所有的tt记录。
记录调用:tt -t org.xgllhz.usercenter.blog.admin.service.impl.BlogServiceImpl listBlogPage,结果如下:

字段说明:
INDEX:记录编号,每个编号对应一次调用。TIMESTAMP:方法执行的本机时间。COST(ms):方法执行耗时。IS-RET:方法是否以正常返回的形式结束。IS-EXP:方法是否以抛异常的形式结束。OBJECT:执行对象的hashCode,注意,它不是盖对象在 jvm 中的内存地址。CLASS:执行的类名。METHOD:执行的方法名。
查看某次调用信息:tt -i 1000,结果如下:
最后别忘了用 tt --delete-all 将所有记录都清除,否则会 OOM。
😊 就这样吧 毁灭吧 很累

1084

被折叠的 条评论
为什么被折叠?



