Arthas常用命令

1,501 阅读14分钟

Arthas命令

我正在参加「掘金·启航计划」

本文中涵盖Arthas中一些常用命令,更改命令请参考Arthas官方文档


dashboard命令

该命令可以显示当前进程的监控信息,实时监控面板,可以根据指定的时间间隔刷新监控的信息。特性如下

  • 阻塞命令:会实时刷新
dashboard

tips:输入dash后按下tab键即可自动补全

1. 参数
参数示例作用
idashborad -i 1000指定刷新的时间间隔,默认5000ms,单位ms
ndashborad -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运行信息

IDNAMEGROUPPRIORITYSTATE%CPUDELTA_TIMETIMEINTERRUPTEDDAEMON
Java级别的线程ID线程名称线程组名称线程优先级1-10之间的数字线程状态线程CPU使用率上次采样之后线程运行增量CPU时间,数据格式为线程运行总CPU时间,数据格式为分:秒线程当前的中断状态是否是daemon(守护)线程
Memoryusedtotalmaxusage 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线程信息模块

IDNAMEGROUPPRIORITYSTATE%CPUDELTA_TIMETIMEINTERRUPTEDDAEMON
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],
]
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-patternmethod-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

命令行输出

[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

命令行输出

[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

那么其它的时间消耗在哪些地方?

  1. 没有被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
    
  2. 非函数调用的指令消耗。比如 i++, getfield等指令。

  3. 在代码执行过程中,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. 注意
  • 需要强调的点

  1. ThreadLocal 信息丢失

    很多框架偷偷的将一些环境变量信息塞到了发起调用线程的 ThreadLocal 中,由于调用线程发生了变化,这些 ThreadLocal 线程信息无法通过 Arthas 保存,所以这些信息将会丢失。

    一些常见的 CASE 比如:鹰眼的 TraceId 等。

  2. 引用的对象

    需要强调的是,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的命令窗口

image-20220613154637788

在默认的情况下,jad命令会展示类的,ClassLoader信息,类地址,和类源码。

  1. 如果只想展示类源码的话可以在jad命令开头添加--source-only 参数即可,这样仅仅只展示类源码。

  2. 如果只想展示类中的某个方法的话,可以在末尾添加方法名称即可

    • jad --source-only cn.yufire.arthas.arthasdemo.controller.TestController trace
  3. 如果不想展示行号注释的话可以新增 --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传送门`