介绍

image.png
官方文档:https://arthas.aliyun.com/doc
下载页面:https://arthas.aliyun.com/doc/download.html
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

    使用实例

  • [1]: 89 math-game.jar
    1. `arthas-boot` Arthas 的启动程序,它启动后,会列出所有的 Java 进程,用户可以选择需要诊断的目标进程。<br />选择第一个进程,输入 `1`,再 `Enter/回车`Attach 成功之后,会打印 `Arthas LOGO`。输入 help 可以获取到更多的帮助信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2436487/1631773200069-29c1a0ab-43b2-46ef-b8b7-93b9309128bb.png#clientId=ua25aae6e-e036-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=410&id=udadaab22&margin=%5Bobject%20Object%5D&name=image.png&originHeight=948&originWidth=2118&originalType=binary&ratio=1&rotation=0&showTitle=false&size=168167&status=done&style=none&taskId=u3e93c488-2b43-4698-9789-4a7cf4441b0&title=&width=916)
    2. <a name="tHMYr"></a>
    3. ## Dashboard 命令
    4. 可以查看当前系统的实时数据面板,输入 `Q` 或者 `Ctrl+C` 可以退出 dashboard命令:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2436487/1631773368842-626b48d1-0af8-4a1e-ac6e-9870d5966f36.png#clientId=ua25aae6e-e036-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=432&id=uc2620149&margin=%5Bobject%20Object%5D&name=image.png&originHeight=864&originWidth=1838&originalType=binary&ratio=1&rotation=0&showTitle=false&size=209027&status=done&style=none&taskId=u79f8de38-8921-4e11-82e6-06f5ca77395&title=&width=919)
    5. <a name="xgonq"></a>
    6. ## Thread 命令
    7. 查看当前线程信息,查看线程的堆栈。
    8. ```shell
    9. $ thread [--all] [-h] [-b] [--lockedMonitors] [--lockedSynchronizers] [-i <value>] [--state <value>] [-n <value>] [id]

    参数说明

    | 参数名称 | 参数说明 | | —- | —- | | id | 线程id | | [n:] | 指定最忙的前N个线程并打印堆栈 | | [b] | 找出当前阻塞其他线程的线程 | | [i ] | 指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200 | | [—all] | 显示所有匹配的线程 |

使用示例

thread 1 命令会打印线程 ID 1 的栈,Arthas 支持管道,可以用 thread 1 | grep 'main(' 查找到 main class:

  1. [arthas@89]$ thread 1
  2. "main" Id=1 TIMED_WAITING
  3. at java.base@15-ea/java.lang.Thread.sleep(Native Method)
  4. at java.base@15-ea/java.lang.Thread.sleep(Thread.java:337)
  5. at java.base@15-ea/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
  6. at app//demo.MathGame.main(MathGame.java:17)
  7. [arthas@89]$ thread 1 | grep 'main('
  8. at app//demo.MathGame.main(MathGame.java:17)

Sc 命令

“Search-Class” 的简写,查看JVM已加载的类信息。

  1. $ sc [-c <value>] [--classLoaderClass <value>] [-d] [-x <value>] [-f] [-h] [-n <value>] [-E] class -pattern

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
[d] 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。
如果一个类被多个ClassLoader所加载,则会出现多次
[E] 开启正则表达式匹配,默认为通配符匹配
[f] 输出当前类的成员变量信息(需要配合参数-d一起使用)
[x:] 指定输出静态变量时属性的遍历深度,默认为 0,即直接使用 toString 输出
[c:] 指定class的 ClassLoader 的 hashcode
[classLoaderClass:] 指定执行表达式的 ClassLoader 的 class name
[n:] 具有详细信息的匹配类的最大数量(默认为100)

使用示例

可以通过 sc 命令来查找JVM里已加载的类,支持通配搜索:

  1. [arthas@89]$ sc *MathGame
  2. demo.MathGame
  3. Affect(row-cnt:1) cost in 9 ms.

通过 -d 参数,可以打印出类加载的具体信息,很方便查找类加载问题:

  1. [arthas@89]$ sc -d *MathGame
  2. class-info demo.MathGame
  3. code-source /root/math-game.jar
  4. name demo.MathGame
  5. isInterface false
  6. isAnnotation false
  7. isEnum false
  8. isAnonymousClass false
  9. isArray false
  10. isLocalClass false
  11. isMemberClass false
  12. isPrimitive false
  13. isSynthetic false
  14. simple-name MathGame
  15. modifier public
  16. annotation
  17. interfaces
  18. super-class +-java.lang.Object
  19. class-loader +-jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44
  20. +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@785affc9
  21. classLoaderHash c387f44
  22. Affect(row-cnt:1) cost in 42 ms.

如果搜索的是接口,会找出所有实现类:

  1. [arthas@38]$ sc java.util.List
  2. com.alibaba.arthas.deps.ch.qos.logback.classic.spi.TurboFilterList
  3. com.alibaba.arthas.deps.ch.qos.logback.core.util.COWArrayList
  4. java.util.AbstractList
  5. java.util.AbstractSequentialList
  6. java.util.ArrayList
  7. java.util.ArrayList$SubList
  8. java.util.Arrays$ArrayList
  9. java.util.Collections$EmptyList
  10. java.util.Collections$SingletonList
  11. java.util.Collections$UnmodifiableList
  12. java.util.Collections$UnmodifiableRandomAccessList
  13. java.util.ImmutableCollections$AbstractImmutableList
  14. java.util.ImmutableCollections$List12
  15. java.util.ImmutableCollections$ListN
  16. java.util.ImmutableCollections$SubList
  17. java.util.LinkedList
  18. java.util.List
  19. java.util.Stack
  20. java.util.Vector
  21. java.util.concurrent.CopyOnWriteArrayList
  22. org.benf.cfr.reader.bytecode.analysis.types.StackTypes
  23. sun.security.jca.ProviderList$3
  24. sun.security.jca.ProviderList$ServiceList
  25. Affect(row-cnt:23) cost in 20 ms.

Sm 命令

“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息,只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。

  1. $ sm [-c <value>] [--classLoaderClass <value>] [-d] [-h] [-n <value>] [-E] class-pattern [method-pattern]

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
[d] 展示每个方法的详细信息
[E] 开启正则表达式匹配,默认为通配符匹配
[c:] 指定class的 ClassLoader 的 hashcode
[classLoaderClass:] 指定执行表达式的 ClassLoader 的 class name
[n:] 具有详细信息的匹配类的最大数量(默认为100)

使用示例

sm 命令可以查找类的具体函数:

  1. [arthas@38]$ sm java.math.RoundingMode
  2. java.math.RoundingMode <init>(Ljava/lang/String;II)V
  3. java.math.RoundingMode values()[Ljava/math/RoundingMode;
  4. java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode;
  5. java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode;
  6. Affect(row-cnt:4) cost in 29 ms.

通过 -d 参数可以打印函数的具体属性:

  1. [arthas@38]$ sm -d java.math.RoundingMode
  2. declaring-class java.math.RoundingMode
  3. constructor-name <init>
  4. modifier private
  5. annotation
  6. parameters java.lang.String
  7. int
  8. int
  9. exceptions
  10. classLoaderHash null
  11. declaring-class java.math.RoundingMode
  12. method-name values
  13. modifier public,static
  14. annotation
  15. parameters
  16. return java.math.RoundingMode[]
  17. exceptions
  18. classLoaderHash null
  19. declaring-class java.math.RoundingMode
  20. method-name valueOf
  21. modifier public,static
  22. annotation
  23. parameters int
  24. return java.math.RoundingMode
  25. exceptions
  26. classLoaderHash null
  27. declaring-class java.math.RoundingMode
  28. method-name valueOf
  29. modifier public,static
  30. annotation
  31. parameters java.lang.String
  32. return java.math.RoundingMode
  33. exceptions
  34. classLoaderHash null
  35. Affect(row-cnt:4) cost in 25 ms.

也可以通过函数名称查找指定的函数:

  1. [arthas@38]$ sm java.math.RoundingMode <init>
  2. java.math.RoundingMode <init>(Ljava/lang/String;II)V
  3. Affect(row-cnt:1) cost in 15 ms.
  4. [arthas@38]$ sm java.math.RoundingMode values
  5. java.math.RoundingMode values()[Ljava/math/RoundingMode;
  6. Affect(row-cnt:1) cost in 19 ms.
  7. [arthas@38]$ sm java.math.RoundingMode valueOf
  8. java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode;
  9. java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode;
  10. Affect(row-cnt:2) cost in 17 ms.

Jad 命令

jad 命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑,在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便,当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解。

  1. $ jad [--classLoaderClass <value>] [-c <value>] [-h] [--hideUnicode] [--lineNumber <value>] [-E] [--source-only] class -pattern [method-name]

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
[c:] 类所属 ClassLoader 的 hashcode
[classLoaderClass:] 指定执行表达式的 ClassLoader 的 class name
[E] 开启正则表达式匹配,默认为通配符匹配

使用示例

可以通过 jad 命令来反编译代码:

  1. [arthas@89]$ jad demo.MathGame
  2. ClassLoader:
  3. +-jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44
  4. +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@785affc9
  5. Location:
  6. /root/math-game.jar
  7. /*
  8. * Decompiled with CFR.
  9. */
  10. package demo;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. import java.util.Random;
  14. import java.util.concurrent.TimeUnit;
  15. public class MathGame {
  16. private static Random random = new Random();
  17. private int illegalArgumentCount = 0;
  18. public static void main(String[] args) throws InterruptedException {
  19. MathGame game = new MathGame();
  20. while (true) {
  21. /*16*/ game.run();
  22. /*17*/ TimeUnit.SECONDS.sleep(1L);
  23. }
  24. ...

通过 --source-only 参数实现只打印源代码,不打印其它信息:

  1. [arthas@89]$ jad --source-only demo.MathGame
  2. /*
  3. * Decompiled with CFR.
  4. */
  5. package demo;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. import java.util.Random;
  9. import java.util.concurrent.TimeUnit;
  10. public class MathGame {
  11. private static Random random = new Random();
  12. private int illegalArgumentCount = 0;
  13. public static void main(String[] args) throws InterruptedException {
  14. MathGame game = new MathGame();
  15. while (true) {
  16. /*16*/ game.run();
  17. /*17*/ TimeUnit.SECONDS.sleep(1L);
  18. }
  19. ...

Watch 命令

让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL 表达式进行对应变量的查看。

  1. $ watch [-b] [-e] [--exclude-class-pattern <value>] [-x <value>] [-f] [-h] [-n <value>] [--listenerId <value>] [-E] [-M <value>] [-s] [-v] class-pattern method-pattern [express] [condition-express]

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
express 观察表达式,默认值:{params, target, returnObj}
condition-express 条件表达式
[b] 方法调用之前观察
[e] 方法异常之后观察
[s] 方法返回之后观察
[f] 方法结束之后(正常返回和异常返回)观察
[E] 开启正则表达式匹配,默认为通配符匹配
[x:] 指定输出结果的属性遍历深度,默认为 1

使用示例

通过 watch 命令可以查看函数的参数/返回值/异常信息:

  1. [arthas@89]$ watch demo.MathGame primeFactors returnObj
  2. Press Q or Ctrl+C to abort.
  3. Affect(class count: 1 , method count: 1) cost in 115 ms, listenerId: 1
  4. method=demo.MathGame.primeFactors location=AtExit
  5. ts=2021-09-16 06:30:20; [cost=0.805757ms] result=@ArrayList[
  6. @Integer[2],
  7. @Integer[61],
  8. @Integer[191],
  9. ]
  10. method=demo.MathGame.primeFactors location=AtExceptionExit
  11. ts=2021-09-16 06:30:21; [cost=0.158301ms] result=null
  12. method=demo.MathGame.primeFactors location=AtExit
  13. ts=2021-09-16 06:30:22; [cost=9.142493ms] result=@ArrayList[
  14. @Integer[207169],
  15. ]
  16. method=demo.MathGame.primeFactors location=AtExit
  17. ts=2021-09-16 06:30:23; [cost=1.599325ms] result=@ArrayList[
  18. @Integer[107053],
  19. ]
  20. method=demo.MathGame.primeFactors location=AtExceptionExit
  21. ts=2021-09-16 06:30:24; [cost=0.105091ms] result=null
  22. method=demo.MathGame.primeFactors location=AtExceptionExit
  23. ts=2021-09-16 06:30:25; [cost=0.069531ms] result=null
  24. method=demo.MathGame.primeFactors location=AtExit
  25. ts=2021-09-16 06:30:26; [cost=0.155641ms] result=@ArrayList[
  26. @Integer[19],
  27. @Integer[3637],
  28. ]

Vmtool 命令

vmtool 利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能。

  1. $ vmtool -a {forceGc, getInstances} [-c <value>] [--classLoaderClass <value>] [--className <value>] [-x <value>] [--express <value>] [-h] [--libPath <value>] [-l <value>]

使用示例

搜索内存对象:

  1. [arthas@89]$ vmtool --action getInstances --className java.lang.String --limit 10
  2. @String[][
  3. @String[vmtool],
  4. @String[vmtool],
  5. @String[ ],
  6. @String[--action],
  7. @String[--action],
  8. @String[ ],
  9. @String[getInstances],
  10. @String[getInstances],
  11. @String[ ],
  12. @String[vmtool --action getInstances --className java.lang.String --limit 10],
  13. ]

Exit/Stop 命令

可以使用 exit 命令退出 Arthas 交互界面,退出后可以通过 java -jar 启动命令再次连接。
exit 命令只是退出当前 session,arthas server 还在目标进程中运行,想完全退出 Arthas,可以执行 stop 命令。

进阶功能

环境搭建

demo-arthas-spring-boot 是一个简单的 spring boot 应用,源代码:https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-arthas-spring-boot

  1. $ wget https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
  2. $ java -jar demo-arthas-spring-boot.jar

Sysprop 命令

sysprop 命令可以用于查看当前JVM的系统属性(System Property)。

  1. $ sysprop [-h] [property-name] [property-value]

使用示例

查看当前JVM的系统属性(System Property):

  1. [arthas@38]$ sysprop
  2. KEY VALUE
  3. ----------------------------------------------------------------------------------------------------
  4. java.specification 15
  5. .version
  6. sun.jnu.encoding UTF-8
  7. java.class.path demo-arthas-spring-boot.jar
  8. java.vm.vendor Oracle Corporation
  9. sun.arch.data.mode 64
  10. l
  11. java.vendor.url https://java.oracle.com/
  12. catalina.useNaming false
  13. user.timezone Etc/UTC
  14. org.jboss.logging. slf4j
  15. provider
  16. java.vm.specificat 15
  17. ion.version
  18. os.name Linux
  19. sun.java.launcher SUN_STANDARD
  20. user.country US
  21. sun.boot.library.p /usr/java/openjdk-15/lib
  22. ath
  23. ...

指定显示单个 key:

  1. [arthas@38]$ sysprop java.version
  2. KEY VALUE
  3. ----------------------------------------------------------------------------------------------------
  4. java.version 15-ea

支持通过 grep 过滤:

  1. [arthas@38]$ sysprop | grep java
  2. java.specification 15
  3. java.class.path demo-arthas-spring-boot.jar
  4. java.vm.vendor Oracle Corporation
  5. java.vendor.url https://java.oracle.com/
  6. java.vm.specificat 15
  7. sun.java.launcher SUN_STANDARD
  8. sun.boot.library.p /usr/java/openjdk-15/lib
  9. sun.java.command demo-arthas-spring-boot.jar
  10. java.specification Oracle Corporation
  11. java.version.date 2020-09-15
  12. java.home /usr/java/openjdk-15
  13. java.vm.compressed 32-bit
  14. java.specification Java Platform API Specification
  15. java.vm.specificat Oracle Corporation
  16. java.awt.headless true
  17. java.protocol.hand org.springframework.boot.loader
  18. java.runtime.versi 15-ea+8-219
  19. java.runtime.name OpenJDK Runtime Environment
  20. java.vm.name OpenJDK 64-Bit Server VM
  21. ...

设置自定义的 value:

  1. [arthas@38]$ sysprop testKey testValue
  2. Successfully changed the system property.
  3. KEY VALUE
  4. ----------------------------------------------------------------------------------------------------
  5. testKey testValue

Sysenv 命令

sysenv 命令可以用于查看当前JVM的环境属性(System Environment Variables)。

  1. $ sysenv [-h] [env-name]

使用示例

sysenv 可以获取环境变量:

  1. [arthas@38]$ sysenv
  2. KEY VALUE
  3. ----------------------------------------------------------------------------------------------------
  4. PATH /usr/java/openjdk-15/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
  5. in:/bin:/usr/bin/gradle/bin:/usr/local/bin/scala-2.13.2/bin:/usr/local/bin/sbt
  6. /bin
  7. LESSCLOSE /usr/bin/lesspipe %s %s
  8. SBT_VERSION 1.3.0
  9. JAVA_HOME /usr/java/openjdk-15
  10. TERM xterm
  11. LANG C.UTF-8
  12. PS1 $
  13. GRADLE_HOME /usr/bin/gradle
  14. MAVEN_HOME /usr/share/maven
  15. GRADLE_USER_HOME /cache
  16. JAVA_SHA256 ab41dd4e616e015ec94f26e4d2c8253f376f5f5d574db811f341abc6c55e333c
  17. SCALA_VERSION 2.13.2
  18. JAVA_VERSION 15-ea+8
  19. ...

Jvm 命令

jvm 命令可以用于查看 jvm 的信息。

  1. $ jvm [-h]

使用示例

jvm 可以打印 jvm 的各种详细信息:

  1. [arthas@38]$ jvm
  2. RUNTIME
  3. ----------------------------------------------------------------------------------------------------
  4. MACHINE-NAME 38@4ba353efdd4e
  5. JVM-START-TIME 2021-09-16 06:46:24
  6. MANAGEMENT-SPEC-VERSION 3.0
  7. SPEC-NAME Java Virtual Machine Specification
  8. SPEC-VENDOR Oracle Corporation
  9. SPEC-VERSION 15
  10. VM-NAME OpenJDK 64-Bit Server VM
  11. VM-VENDOR Oracle Corporation
  12. VM-VERSION 15-ea+8-219
  13. INPUT-ARGUMENTS []
  14. CLASS-PATH demo-arthas-spring-boot.jar
  15. BOOT-CLASS-PATH
  16. LIBRARY-PATH /usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib
  17. ...

Ognl 命令

执行ognl表达式。

  1. $ ognl [-c <value>] [--classLoaderClass <value>] [-x <value>] [-h] express

参数说明

express 执行的表达式
[c:] 执行表达式的 ClassLoader 的 hashcode,默认值是SystemClassLoader
[classLoaderClass:] 指定执行表达式的 ClassLoader 的 class name
[x] 结果对象的展开层次,默认值1

使用示例

调用静态方法

  1. [arthas@39]$ ognl '@java.lang.Math@random()'
  2. @Double[0.36922744380446826]

获取类的静态字段

方法1:使用 -c 参数指定 ClassLoader hashcode
使用 sc 命令获取类的 ClassLoader hashcode:

  1. [arthas@8833]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
  2. classLoaderHash 5b2133b1

使用 ognl -c <class loader hashcode> <express> 获取类的静态字段:

  1. [arthas@8833]$ ognl -c 5b2133b1 @com.example.demo.arthas.user.UserController@logger
  2. @Logger[
  3. serialVersionUID=@Long[5454405123156820674],
  4. FQCN=@String[ch.qos.logback.classic.Logger],
  5. name=@String[com.example.demo.arthas.user.UserController],
  6. level=null,
  7. effectiveLevelInt=@Integer[20000],
  8. parent=@Logger[Logger[com.example.demo.arthas.user]],
  9. childrenList=null,
  10. aai=null,
  11. additive=@Boolean[true],
  12. loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
  13. ]

对于只有唯一实例的 ClassLoader 可以通过 --classLoaderClass 指定 class name,使用起来更加方便:

  1. [arthas@8833]$ ognl -classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader @org.springframework.boot.SpringApplication@logger
  2. serialVersionUID=@Long[-2379157579039314822],
  3. name=@String[org.springframework.boot.SpringApplication],
  4. logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
  5. FQCN=@String[org.apache.commons.logging.impl.SLF4JLocationAwareLog],
  6. ]

还可以通过 -x 参数控制返回值的展开层数。比如:

  1. [arthas@8833]$ ognl -classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -x 2 @@SLF4JLocationAwareLog[t.SpringApplication@logger
  2. serialVersionUID=@Long[-2379157579039314822],
  3. name=@String[org.springframework.boot.SpringApplication],
  4. logger=@Logger[
  5. serialVersionUID=@Long[5454405123156820674],
  6. FQCN=@String[ch.qos.logback.classic.Logger],
  7. name=@String[org.springframework.boot.SpringApplication],
  8. level=null,
  9. effectiveLevelInt=@Integer[20000],
  10. parent=@Logger[Logger[org.springframework.boot]],
  11. childrenList=null,
  12. aai=null,
  13. additive=@Boolean[true],
  14. loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
  15. ],
  16. FQCN=@String[org.apache.commons.logging.impl.SLF4JLocationAwareLog],
  17. ]

案例

排查函数调用异常

现象

访问 http://localhost/user/0 会返回异常:

  1. Something went wrong: 500 Internal Server Error

通过 watch com.example.demo.arthas.user.UserController * '{params, throwExp}' 命令监控方法的参数、异常信息。

  1. 第一个参数是类名,支持通配
  2. 第二个参数是函数名,支持通配

这时再访问 http://localhost/user/0

  1. [arthas@8833]$ watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
  2. Press Q or Ctrl+C to abort.
  3. Affect(class count: 1 , method count: 2) cost in 190 ms, listenerId: 1
  4. method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
  5. ts=2021-09-18 13:48:44; [cost=4.921205ms] result=@ArrayList[
  6. @Object[][isEmpty=false;size=1],
  7. @IllegalArgumentException[java.lang.IllegalArgumentException: id < 1],
  8. ]

发现抛出的是 IllegalArgumentException。

返回值表达式

上面的例子中,第三个参数是返回值表达式,它实际上是一个 ognl 表达式,它支持一些内置对象:

  • loader
  • clazz
  • method
  • target
  • params
  • returnObj
  • throwExp
  • isBefore
  • isThrow
  • isReturn

可以利用这些内置对象来组成不同的表达式。比如返回一个数组:

  1. [arthas@8833]$ watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'
  2. Press Q or Ctrl+C to abort.
  3. Affect(class count: 1 , method count: 2) cost in 119 ms, listenerId: 2
  4. method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
  5. ts=2021-09-18 14:27:20; [cost=1.548078ms] result=@ArrayList[
  6. @Integer[0],
  7. @UserController[com.example.demo.arthas.user.UserController@5d720b2f],
  8. null,
  9. ]
  10. method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
  11. ts=2021-09-18 14:27:22; [cost=1.069647ms] result=@ArrayList[
  12. @Integer[1],
  13. @UserController[com.example.demo.arthas.user.UserController@5d720b2f],
  14. @User[com.example.demo.arthas.user.User@2457a06b],
  15. ]

条件表达式

watch 命令支持在第 4 个参数里写条件表达式,比如:

  1. [arthas@8833]$ watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'
  2. Press Q or Ctrl+C to abort.
  3. Affect(class count: 1 , method count: 2) cost in 31 ms, listenerId: 3
  4. method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
  5. ts=2021-09-18 14:31:44; [cost=0.330385ms] result=@User[
  6. id=@Integer[101],
  7. name=@String[name101],
  8. ]

当访问 http://localhost/user/1 时,watch 命令没有输出,
当访问 http://localhost/user/101 时,watch 命令会打印输出。

当异常时捕获

watch 命令支持 -e 选项,表示只捕获抛出异常时的请求:
watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e

按照耗时进行过滤

watch命令支持按请求耗时进行过滤,比如:
watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'

热更新代码

现象

下面介绍通过 jad/mc/redefine 命令实现动态更新代码的功能。
目前,访问 http://localhost/user/0 ,会返回500异常:

  1. Something went wrong: 500 Internal Server Error

jad 反编译 UserController

jad 反编译的结果保存在 /tmp/UserController.java 文件里:

  1. [arthas@8833]$ jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java

编辑文件内容,当 id 小于 1 时,不抛出异常:

  1. @GetMapping(value={"/user/{id}"})
  2. public User findUserById(@PathVariable Integer id) {
  3. logger.info("id: {}", (Object)id);
  4. if (id != null && id < 1) {
  5. // throw new IllegalArgumentException("id < 1");
  6. }
  7. return new User(id.intValue(), "name" + id);
  8. }

sc 查找加载 UserController 的 ClassLoader

  1. [arthas@8833]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
  2. classLoaderHash 5b2133b1

mc 编译

使用 mc(Memory Compiler) 命令来编译,并且通过 -c 或者 --classLoaderClass 参数指定ClassLoader:

  1. [arthas@8833]$ mc -c 5b2133b1 /tmp/UserController.java -d /tmp
  2. Memory compiler output:
  3. /tmp/com/example/demo/arthas/user/UserController.class
  4. Affect(row-cnt:1) cost in 2271 ms.

redefine 重新加载 class

  1. [arthas@8833]$ redefine /tmp/com/example/demo/arthas/user/UserController.class
  2. redefine success, size: 1, classes:
  3. com.example.demo.arthas.user.UserController

测试

访问 http://localhost/user/0 可以正常返回了:

  1. {"id":0, "name":"name0"}

动态更新应用 Logger Level

现象

UserController 的 logger level 为 null,需要将其进行赋值为 debug。

用 sc 获取 UserController 的 ClassLoader

  1. [arthas@1145]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
  2. classLoaderHash 5b2133b1

用 ognl 获取 logger 的值

  1. [arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger'
  2. @Logger[
  3. serialVersionUID=@Long[5454405123156820674],
  4. FQCN=@String[ch.qos.logback.classic.Logger],
  5. name=@String[com.example.demo.arthas.user.UserController],
  6. level=null,
  7. effectiveLevelInt=@Integer[20000],
  8. parent=@Logger[Logger[com.example.demo.arthas.user]],
  9. childrenList=null,
  10. aai=null,
  11. additive=@Boolean[true],
  12. loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
  13. ]

可以知道 UserController@logger 实际使用的是 logback。可以看到 level=null,则说明实际最终的 level 是从 root logger 里来的。

使用 ognl 调用 set 方法进行赋值

  1. [arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
  2. null

再次获取 logger 的值

再次获取 UserController@logger,可以发现已经是 DEBUG 了:

  1. [arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger'
  2. @Logger[
  3. serialVersionUID=@Long[5454405123156820674],
  4. FQCN=@String[ch.qos.logback.classic.Logger],
  5. name=@String[com.example.demo.arthas.user.UserController],
  6. level=@Level[DEBUG],
  7. effectiveLevelInt=@Integer[10000],
  8. parent=@Logger[Logger[com.example.demo.arthas.user]],
  9. childrenList=null,
  10. aai=null,
  11. additive=@Boolean[true],
  12. loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
  13. ]

修改 logback 的全局 logger level

  1. [arthas@1145]$ ognl -c 5b2133b1 '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
  2. null

获取 logback 的全局 logger level

再次获取 LoggerFactory@getLogger,可以发现已经是 DEBUG 了:

  1. [arthas@1145]$ ognl -c 5b2133b1 '@org.slf4j.LoggerFactory@getLogger("root")'
  2. @Logger[
  3. serialVersionUID=@Long[5454405123156820674],
  4. FQCN=@String[ch.qos.logback.classic.Logger],
  5. name=@String[ROOT],
  6. level=@Level[DEBUG],
  7. effectiveLevelInt=@Integer[10000],
  8. parent=null,
  9. childrenList=@CopyOnWriteArrayList[isEmpty=false;size=3],
  10. aai=@AppenderAttachableImpl[ch.qos.logback.core.spi.AppenderAttachableImpl@9f47a2a],
  11. additive=@Boolean[true],
  12. loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
  13. ]