Arthas命令
我正在参加「掘金·启航计划」
本文中涵盖Arthas
中一些常用命令,更改命令请参考Arthas
官方文档
dashboard命令
该命令可以显示当前进程的监控信息,实时监控面板,可以根据指定的时间间隔刷新监控的信息。特性如下
- 阻塞命令:会实时刷新
dashboard
tips:输入dash后按下tab键即可自动补全
1. 参数
参数 | 示例 | 作用 |
---|---|---|
i | dashborad -i 1000 | 指定刷新的时间间隔,默认5000ms,单位ms |
n | dashborad -n 2 | 指定刷新的次数 |
JVM内部线程包括下面几种:
- JIT编译线程: 如
C1 CompilerThread0
,C2 CompilerThread0
- GC线程: 如
GC Thread0
,G1 Young RemSet Sampling
- 其它内部线程: 如
VM Periodic Task Thread
,VM Thread
,Service Thread
输出如下
ID NAME GROUP PRIORI STATE %CPU DELTA_ TIME INTER DAEMON
63 Timer-for-arthas-da system 5 RUNNAB 0.35 0.017 0:0.02 false true
-1 C1 CompilerThread3 - -1 - 0.17 0.008 0:1.26 false true
-1 VM Periodic Task Th - -1 - 0.06 0.003 0:3.59 false true
61 arthas-NettyHttpTel system 5 RUNNAB 0.06 0.002 0:0.07 false true
26 Catalina-utility-1 main 1 WAITIN 0.01 0.000 0:0.52 false false
27 Catalina-utility-2 main 1 TIMED_ 0.01 0.000 0:0.56 false false
-1 VM Thread - -1 - 0.01 0.000 0:0.24 false true
-1 C2 CompilerThread1 - -1 - 0.01 0.000 0:0.04 false true
-1 C2 CompilerThread2 - -1 - 0.01 0.000 0:0.05 false true
40 http-nio-8080-Polle main 5 RUNNAB 0.0 0.000 0:0.35 false true
Memory used total max usage GC
heap 73M 247M 2.01% 5
ps_eden_space 58M 125M 4.35% gc.ps_scavenge.time 64
ps_survivor_spac 0K 10752 1075 0.00% (ms)
e K 2K gc.ps_marksweep.cou 2
ps_old_gen 14M 111M 0.55% nt
nonheap 50M 53M -1 gc.ps_marksweep.tim 93
code_cache 6M 6M 128M 5.07% e(ms)
Runtime
os.name Mac OS X
os.version 12.2.1
java.version 1.8.0_302
2. 返回参数说明
该命令的输出分为三块,线程信息、内存信息、JVM运行信息
ID | NAME | GROUP | PRIORITY | STATE | %CPU | DELTA_TIME | TIME | INTERRUPTED | DAEMON |
---|---|---|---|---|---|---|---|---|---|
Java级别的线程ID | 线程名称 | 线程组名称 | 线程优先级1-10之间的数字 | 线程状态 | 线程CPU使用率 | 上次采样之后线程运行增量CPU时间,数据格式为秒 | 线程运行总CPU时间,数据格式为分:秒 | 线程当前的中断状态 | 是否是daemon(守护)线程 |
Memory | used | total | max | usage GC |
---|---|---|---|---|
内存栈名称 | 使用量内存 | 总量内存 | 最大内存 | 垃圾回收器 |
Runtime |
---|
JVM运行信息 |
thread命令
查看当前进程的所有线程信息,查看线程的堆栈,特性如下
- 阻塞命令
- 非阻塞命令
1. 参数
参数名称 | 参数说明 |
---|---|
id | 线程id |
n | 指定最忙的前N个线程,并打印堆栈 |
b | 找出当前阻塞其他线程的线程 |
i <value> | 指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200 |
--all | 显示所有匹配的线程 |
--state | 根据线程状态筛选 RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, NEW, TERMINATED |
输出如下
thread
Threads Total: 48, NEW: 0, RUNNABLE: 13, BLOCKED: 0, WAITING: 16, TIMED_WAITING: 4, TERMINATED: 0, Internal threads:
15
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIM TIME INTERRUP DAEMON
62 arthas-command-execute system 5 RUNNABLE 0.34 0.000 0:0.067 false true
-1 VM Periodic Task Thread - -1 - 0.07 0.000 0:5.149 false true
40 http-nio-8080-Poller main 5 RUNNABLE 0.03 0.000 0:0.510 false true
30 http-nio-8080-exec-1 main 5 WAITING 0.0 0.000 0:0.000 false true
31 http-nio-8080-exec-2 main 5 WAITING 0.0 0.000 0:0.000 false true
32 http-nio-8080-exec-3 main 5 WAITING 0.0 0.000 0:0.000 false true
33 http-nio-8080-exec-4 main 5 WAITING 0.0 0.000 0:0.000 false true
34 http-nio-8080-exec-5 main 5 WAITING 0.0 0.000 0:0.000 false true
35 http-nio-8080-exec-6 main 5 WAITING 0.0 0.000 0:0.000 false true
36 http-nio-8080-exec-7 main 5 WAITING 0.0 0.000 0:0.000 false true
...
第一行会显示线程总信息
- Total:线程总数
- 各种线程运行状态的总数统计
下方的展示字段与命令:dashboard
一样,可以说是完整版的dashboard
线程信息模块
ID | NAME | GROUP | PRIORITY | STATE | %CPU | DELTA_TIME | TIME | INTERRUPTED | DAEMON |
---|---|---|---|---|---|---|---|---|---|
Java级别的线程ID | 线程名称 | 线程组名称 | 线程优先级1-10之间的数字 | 线程状态 | 线程CPU使用率 | 上次采样之后线程运行增量CPU时间,数据格式为秒 | 线程运行总CPU时间,数据格式为分:秒 | 线程当前的中断状态 | 是否是daemon(守护)线程 |
2. 返回参数说明
thread id
线程的id,在thread命令中可以查看到,用于查看线程的堆栈信息
输出线程的名称、以及状态、以及堆栈信息
thread 13
"测试Arthas的thread命令线程" Id=13 TIMED_WAITING
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.yufire.arthas.arthasdemo.ArthasDemoApplication.lambda$main$0(ArthasDemoApplication.java:20)
at cn.yufire.arthas.arthasdemo.ArthasDemoApplication$$Lambda$3/1694819250.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
thread -i
指定采样时间间隔
thread -i 1000
: 统计最近1000ms内的线程CPU时间。thread -n 3 -i 1000
: 列出1000ms内最忙的3个线程栈
ongl 命令
ognl
命令可以执行ognl
表达式,可以调用静态方法、查看静态字段,或者调用静态方法查看返回值等功能
1. 调用静态方法
ognl '@java.lang.System@out.println("hello")'
null
2. 获取静态字段
ognl '@demo.MathGame@random'
@Random[
serialVersionUID=@Long[3905348978240129619],
seed=@AtomicLong[125451474443703],
multiplier=@Long[25214903917],
addend=@Long[11],
mask=@Long[281474976710655],
DOUBLE_UNIT=@Double[1.1102230246251565E-16],
BadBound=@String[bound must be positive],
BadRange=@String[bound must be greater than origin],
BadSize=@String[size must be non-negative],
seedUniquifier=@AtomicLong[-3282039941672302964],
nextNextGaussian=@Double[0.0],
haveNextNextGaussian=@Boolean[false],
serialPersistentFields=@ObjectStreamField[][isEmpty=false;size=3],
unsafe=@Unsafe[sun.misc.Unsafe@28ea5898],
seedOffset=@Long[24],
]
3. 执行多行表达式
执行多行表达式,赋值给临时变量,返回一个List:
$ ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}'
@ArrayList[
@String[/opt/java/8.0.181-zulu/jre],
@String[OpenJDK Runtime Environment],
]
- 特殊用法请参考:github.com/alibaba/art…
- OGNL表达式官网:commons.apache.org/proper/comm…
1. 观察表达式写法依据
官方文档:传送门
无论是匹配表达式也好、观察表达式也罢,他们核心判断变量都是围绕着一个 Arthas
中的通用通知对象 Advice
进行。
public class Advice {
// 类加载器
private final ClassLoader loader;
// 类
private final Class<?> clazz;
// arthas附着的方法 本次调用方法反射引用
private final ArthasMethod method;
// 本次调用类的实例 new过的哦
private final Object target;
// 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组
private final Object[] params;
// 本次调用返回的对象。当且仅当 isReturn==true 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 void,则值为 null
private final Object returnObj;
// 本次调用抛出的异常。当且仅当 isThrow==true 成立时有效,表明方法调用是以抛出异常的方式结束。
private final Throwable throwExp;
// 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时 isBefore==true 成立,同时 isThrow==false 和 isReturn==false,因为在方法刚开始时,还无法确定方法调用将会如何结束。
private final boolean isBefore;
// 辅助判断标记,当前的方法调用以抛异常的形式结束。
private final boolean isThrow;
// 辅助判断标记,当前的方法调用以正常返回的形式结束。
private final boolean isReturn;
// getter/setter
}
所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合 OGNL 脚本语法或者引入了不在表格中的变量,则退出命令的执行;用户可以根据当前的异常信息修正条件表达式
或观察表达式
watch命令
watch
命令用于监控方法的执行过程,获得请求参数以及返回值
1. 参数
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 函数名表达式匹配 |
express | 观察表达式,默认值:{params, target, returnObj} 此参数决定了控制台的输出内容 ognl表达式 |
condition-express | 条件表达式 ognl表达式 |
b | 在函数调用之前观察 before,执行此命令得不到方法的返回值 |
e | 在函数异常之后观察 exception |
s | 在函数返回之后观察 |
f | 在函数结束之后(正常返回和异常返回)观察 finishd |
E | 开启正则表达式匹配,默认为通配符匹配 |
x | 输出对象的深度 指定输出结果的属性遍历深度,默认为 1,最大值是4 |
#cost | 方法执行耗时筛选 例子#cost>1000 格式ms |
2. 基础用法
代码:
package cn.yufire.arthas.arthasdemo.controller;
... 省略import
@GetMapping("watch")
public Map<String, String> watch(String name, String age) {
HashMap<String, String> hashMap = new HashMap<>(1);
hashMap.put(name, age);
return hashMap;
}
命令:
watch cn.yufire.arthas.arthasdemo.controller.TestController watch
- 监控包
cn.yufire.arthas.arthasdemo.controller.TestController
下的watch
方法- 默认监控 params, target, returnObj
- params:请求参数信息
- target:本次调用类的实例
- returnObj:方法的返回信息
- 监控深度 1,内存地址级别
发送GET请求:http://localhost:8080/api/watch?age=1&name=Yu
[arthas@55193]$ watch cn.yufire.arthas.arthasdemo.controller.TestController watch
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 52 ms, listenerId: 23
method=cn.yufire.arthas.arthasdemo.controller.TestController.watch location=AtExit
ts=2022-05-27 16:19:36; [cost=0.061166ms] result=@ArrayList[
@Object[][isEmpty=false;size=2],
@TestController[cn.yufire.arthas.arthasdemo.controller.TestController@1c773ddc],
@HashMap[isEmpty=false;size=1],
]
输出内容分为两部分:附着成功的提示、监控的实时输出
1.附着成功提示
-
Press Q or Ctrl+C to abort.
:按下Q
或者ctrl+c
结束监控 -
Affect(class count: 1 , method count: 1) cost in 52 ms, listenerId: 23
附着了一个类,一个方法,耗时52毫秒
2.实时监控输出
ts=2022-05-27 16:19:36; [cost=0.061166ms] result=@ArrayList
ts=执行时间,cost=方法耗时,result=输出信息(根据express)观察表达式输出,该打印为数组- 不同观察表达式的输出由逗号,分割展示
- 展示深度由 -x 指令设置,如果深度过浅的话可能看不到参数的具体值,只能看到参数的内存地址
3. 根据参数过滤进行监控
命令
watch cn.yufire.arthas.arthasdemo.controller.TestController watch {params,returnObj} 'params[0]==1' -x 2
监控包
cn.yufire.arthas.arthasdemo.controller.TestController
下的watch
方法只显示 params,returnObj
- params:请求参数信息
- returnObj:方法的返回信息
过滤第0个参数为1的方法执行
这个0代表的是在方法形参中位置索引0的参数
public Map<String, String> watch(String name, String age);
params[0]
即代表name
参数监控深度为 2
测试
-
发送GET请求:http://localhost:8080/api/watch?age=1&name=Yu
- 此时控制台无任何输出内容,因为条件不匹配
-
发送GET请求:http://localhost:8080/api/watch?age=1&name=1
-
此时控制台输出监控的参数信息
-
method=cn.yufire.arthas.arthasdemo.controller.TestController.watch location=AtExit ts=2022-05-27 16:51:22; [cost=0.066833ms] result=@ArrayList[ @Object[][ @String[1], @String[1], ], @HashMap[ @String[1]:@String[1], ], ]
-
trace命令
可以显示方法的调用链信息,方法内部调用路径,并输出方法路径上的每个节点上耗时。
trace
命令能主动搜索 class-pattern
/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
1. 参数
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
n | 命令执行次数 |
#cost | 方法执行耗时筛选 例子#cost>1000 格式ms |
--skipJDKMethod | 是否跳过jdk 函数,默认true 跳过,设置为false 可以显示jdk 函数的调用链信息 |
2. 基础用法
命令
trace cn.yufire.arthas.arthasdemo.controller.TestController trace
- 监控包
cn.yufire.arthas.arthasdemo.controller.TestController
下的trace
方法- 默认排除JDK自带方法
- 发送GET请求:http://localhost:8080/api/trace?age=1&name=Yu
命令行输出
[arthas@49626]$ trace cn.yufire.arthas.arthasdemo.controller.TestController trace
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 100 ms, listenerId: 1
`---ts=2022-06-01 15:40:19;thread_name=http-nio-8080-exec-2;id=1c;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@514eedd8
`---[0.800125ms] cn.yufire.arthas.arthasdemo.controller.TestController:trace()
`---[9.78% 0.07825ms ] org.springframework.util.StringUtils:toStringArray() #26
输出的内容包括
- 方法的调用链信息
- 此方法执行耗时在完整调用链中的所占百分比
- 每一步(每一行代码)的执行耗时
- 耗时最长的会将字体颜色赋为红色,以便提醒使用者这一行耗时最长
- 调用方法的全地址,以及这行代码在你项目源码里的行数
源码
package cn.yufire.arthas.arthasdemo.controller;
... 省略import
@GetMapping("trace")
public Map<String, String> trace(String name, String age) {
HashMap<String, String> hashMap = new HashMap<>(1);
hashMap.put(name, age);
String[] strings = StringUtils.toStringArray(Arrays.asList("1", "2", "3", "4"));
System.out.println(Arrays.toString(strings));
return hashMap;
}
3. 不跳过JDK
函数
tips:你可能会发现命令输出的内容,并不包含
JDK
自带的方法调用信息,这是为啥嘞❓
- 官方文档中写道:默认情况下,
trace
不会包含jdk
里的函数调用,如果希望trace jdk
里的函数,需要显式设置--skipJDKMethod false
修改命令再次trace
函数
命令
trace cn.yufire.arthas.arthasdemo.controller.TestController trace --skipJDKMethod false
- 监控包
cn.yufire.arthas.arthasdemo.controller.TestController
下的trace
方法- 不排除JDK自带方法
- 发送GET请求:http://localhost:8080/api/trace?age=1&name=Yu
命令行输出
[arthas@49626]$ trace cn.yufire.arthas.arthasdemo.controller.TestController trace --skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 107 ms, listenerId: 2
`---ts=2022-06-01 15:48:00;thread_name=http-nio-8080-exec-5;id=1f;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@514eedd8
`---[0.353625ms] cn.yufire.arthas.arthasdemo.controller.TestController:trace()
+---[4.42% 0.015625ms ] java.util.HashMap:<init>() #24
+---[5.43% 0.019209ms ] java.util.HashMap:put() #25
+---[6.29% 0.02225ms ] java.util.Arrays:asList() #26
+---[12.88% 0.045541ms ] org.springframework.util.StringUtils:toStringArray() #26
+---[6.41% 0.022667ms ] java.util.Arrays:toString() #27
`---[21.68% 0.076667ms ] java.io.PrintStream:println() #27
可以看到此次的输出内容已经包含JDK
的方法了,并且也可显示方法执行的耗时了
4. 执行耗时不准确的问题 原文
比如下面的结果里:
0.705196 > (0.152743 + 0.145825)
$ trace demo.MathGame run -n 1 Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 66 ms, listenerId: 1 `---ts=2021-02-08 11:27:36;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@232204a1 `---[0.705196ms] demo.MathGame:run() +---[0.152743ms] demo.MathGame:primeFactors() #24 `---[0.145825ms] demo.MathGame:print() #25
那么其它的时间消耗在哪些地方?
没有被trace到的函数。比如
java.*
下的函数调用默认会忽略掉。通过增加--skipJDKMethod false
参数可以打印出来。
$ trace demo.MathGame run --skipJDKMethod false Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 35 ms, listenerId: 2 `---ts=2021-02-08 11:27:48;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@232204a1 `---[0.810591ms] demo.MathGame:run() +---[0.034568ms] java.util.Random:nextInt() #23 +---[0.119367ms] demo.MathGame:primeFactors() #24 [throws Exception] +---[0.017407ms] java.lang.StringBuilder:<init>() #28 +---[0.127922ms] java.lang.String:format() #57 +---[min=0.01419ms,max=0.020221ms,total=0.034411ms,count=2] java.lang.StringBuilder:append() #57 +---[0.021911ms] java.lang.Exception:getMessage() #57 +---[0.015643ms] java.lang.StringBuilder:toString() #57 `---[0.086622ms] java.io.PrintStream:println() #57
非函数调用的指令消耗。比如
i++
,getfield
等指令。在代码执行过程中,JVM可能出现停顿,比如GC,进入同步块等。
tt 命令
tt
命令可以记录方法执行时的运行环境,可以用于复现接口请求。
watch
虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。
这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。
于是乎,TimeTunnel 命令就诞生了
1. 简单使用
tt -t cn.yufire.arthas.arthasdemo.controller.TestController trace
- 监控并记录方法
trace()
的每次请求执行情况- 命令窗口显示如下
[arthas@34484]$ tt cn.yufire.arthas.arthasdemo.controller.TestController trace -t
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 66 ms, listenerId: 6
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1076 2022-06-13 15:16:21 0.237084 true false 0x1f26babf TestController trace
1077 2022-06-13 15:16:30 0.124292 true false 0x1f26babf TestController trace
1078 2022-06-13 15:16:30 0.082125 true false 0x1f26babf TestController trace
1079 2022-06-13 15:16:30 0.094417 true false 0x1f26babf TestController trace
1080 2022-06-13 15:16:30 0.083584 true false 0x1f26babf TestController trace
1081 2022-06-13 15:16:31 0.070166 true false 0x1f26babf TestController trace
- 表格字段说明
表格字段 | 字段解释 |
---|---|
INDEX | 时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。 |
TIMESTAMP | 方法执行的本机时间,记录了这个时间片段所发生的本机时间 |
COST(ms) | 方法执行的耗时 |
IS-RET | 方法是否以正常返回的形式结束 |
IS-EXP | 方法是否以抛异常的形式结束 |
OBJECT | 执行对象的hashCode() ,注意,曾经有人误认为是对象在JVM中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体 |
CLASS | 执行的类名 |
METHOD | 执行的方法名 |
2. 检索调用记录
-
使用
-l
参数可以查看tt
命令记录的所有调用信息 -
使用
-s
参数可以使用ognl
表达式进行搜索 同样的,搜索表达式的核心对象依旧是Advice
对象。 -
使用
-i
参数,后边跟上INDEX
可以查看记录调用的详情 -
使用
-p
参数可以重做一次调用,通过--replay-times
指定 调用次数,通过--replay-interval
指定多次调用间隔(单位ms, 默认1000ms) -
使用
-w
参数,可以使用ognl
表达式操作已经记录的调用信息。还可以调用静态方法-
tt -i 1001 -w '@cn.yufire.demo.util.DateUtil.getCurrentDate(new Date())'
-
3. 高级用法
获取SpringContext,并调用容器内Bean
的方法。
案例一:在SpringMVC环境下,获取
ApplicationContext
在类
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
中的invokeHandlerMethod()
在每次请求是都会进行调用,且此类中含有方法getApplicationContext()
,可以帮助我们获取Spring的上下文使用方式:
- 使用tt命令记录
invokeHandlerMethod()
的请求记录
- 注意:建议添加
-n
参数,防止请求量过大,导致JVM
内存瞬间被撑爆,从而导致生产BUG
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 1
- 获取
tt
命令记录的INDEX
并使用ongl
表达式调用getApplicationContext()
方法
tt -i 1000 -w '#context=target.getApplicationContext(),#context.getBean("userServiceImpl").getUserInfo(1)'
- 此处的
target
即代表ongl
的上下文对象,也就是RequestMappingHandlerAdapter
类的Class
实例,所以可以直接使用次实例调用内部方法- 使用
#context
接收Spring
容器- 紧接着在调用,
getBean()
并传入Bean
的名称,并调用次Bean
内的方法
案例二:直接从静态方法中获取
ApplicationContext
假设你的项目内存在静态方法可以获取
ApplicationContext
,那么我们可以直接使用ongl
命令调用即可ognl '#context=@com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory@contexts.iterator.next, #context.getBean("userServiceImpl").findUser(1)' @User[ id=@Integer[1], name=@String[Deanna Borer], ]
4. 注意
需要强调的点
ThreadLocal 信息丢失
很多框架偷偷的将一些环境变量信息塞到了发起调用线程的 ThreadLocal 中,由于调用线程发生了变化,这些 ThreadLocal 线程信息无法通过 Arthas 保存,所以这些信息将会丢失。
一些常见的 CASE 比如:鹰眼的 TraceId 等。
引用的对象
需要强调的是,
tt
命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在tt
查看的时候将无法看到当时最准确的值。这也是为什么watch
命令存在的意义。
jad 命令
jad
命令可以帮助我们反编译已经加载类的源码,可以帮助我们进行线上查看代码是否生效,或者是否运行的是最新的代码。
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑。在Artahs Console
上,反编译的代码带高亮语法,更便于查看。
1. 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] | 类所属 ClassLoader 的 hashcode |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
2. 简单使用
jad cn.yufire.arthas.arthasdemo.controller.TestController
输出如下 tips: 此为Mac Bash的命令窗口
在默认的情况下,jad
命令会展示类的,ClassLoader
信息,类地址,和类源码。
-
如果只想展示类源码的话可以在
jad
命令开头添加--source-only
参数即可,这样仅仅只展示类源码。 -
如果只想展示类中的某个方法的话,可以在末尾添加方法名称即可
jad --source-only cn.yufire.arthas.arthasdemo.controller.TestController trace
-
如果不想展示行号注释的话可以新增
--lineNumber false
即可,默认为true
即展示行号
mc 命令
Memory Compiler/内存编译器,编译
.java
文件生成.class
。
mc
命令可以在内存中将本地磁盘上的.java
文件编译成.class
文件,并且可以配合retransform
命令,实现热更新代码(并不推荐mc
+retransform
)。
1. 简单使用
mc -d /tmp/output /tmp/ClassA.java /tmp/ClassB.java
-d
指定输出的目录- 后边为多个待编译的
java
源文件
使用-c [classLoaderId]
可以指定编译的ClassLoader
mc -c 327a647b /tmp/Test.java
retransform 命令
该命令功能十分强大,它可以将外部的.class
文件动态加载到jvm
中去,可以将原有的类进行覆盖。
包括Spring
中的Bean
也可以使用该命令进行替换。
1. 简单使用
替换指定的class文件到jvm
命令:retransform /tmp/MathGame.class
-
后边为
.class
文件的绝对路径 -
输出:显示替换状态,以及替换数量
-
retransform success, size: 1, classes: demo.MathGame
2. 展示entry
每当执行一次retransform
就会生成一个retransform entry
,作用是为了可以记录替换历史,和还原替换内容。
命令:retransform -l
-
输出
-
Id ClassName TransformCount LoaderHash LoaderClassName 1 demo.MathGame 1 null null
3. 删除entry
使用-d [entryId]
即可删除指定的entry
命令:retransform -d 1
使用--deleteAll
可以删除所有的entry
4. 显示触发retransform
注意:对于同一个类,当存在多个 retransform entry时,如果显式触发 retransform ,则最后添加的entry生效(id最大的)。
命令:retransform --classPattern demo.MathGame
5. 还原类
如果对某个类执行 retransform 之后,想消除影响,则需要:
- 删除这个类对应的
retransform entry
- 重新触发
retransform
如果不清除掉所有的 retransform entry,并重新触发 retransform ,则arthas stop时,retransform过的类仍然生效。
6. 注意
一、建议在本地使用IDEA
将.java
文件编译后再上传至服务器,进行retransform
。如果直接使用mc
命令编译,则会经常报错:类找不到,方法找不到。
二、retransform
的限制
-
不允许新增加字段和方法
-
正在运行的函数如果不退出则
retransform
过的代码不会生效-
例如:
while
中的代码块 -
public class MathGame { public static void main(String[] args) throws InterruptedException { MathGame game = new MathGame(); while (true) { game.run(); TimeUnit.SECONDS.sleep(1); // 这个不生效,因为代码一直跑在 while里 System.out.println("in loop"); } } public void run() throws InterruptedException { // 这个生效,因为run()函数每次都可以完整结束 System.out.println("call run()"); try { int number = random.nextInt(); List<Integer> primeFactors = primeFactors(number); print(number, primeFactors); } catch (Exception e) { System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); } }
-
Arthas案例
在Github
上,有许多用户上传到Artahs
使用案例,可以帮助我们更好的了解和使用Arthas
传送门`