介绍
官方文档:https://arthas.aliyun.com/doc
下载页面:https://arthas.aliyun.com/doc/download.html
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从JVM内查找某个类的实例?
使用实例
- Arthas 实践—jad/mc/redefine 线上热更新一条龙
- Alibaba Arthas 实践—获取到 Spring Context,然后为所欲为
- Arthas 实践—快速排查 Spring Boot 应用 404/401 问题
- 当 Dubbo 遇上 Arthas:排查问题的实践
- 使用 Arthas 抽丝剥茧排查线上应用日志打满问题
- 深入 Spring Boot:利用 Arthas 排查 NoSuchMethodError
基础功能
环境搭建
准备一个测试的 Java 程序
下载math-game.jar
,再用java -jar
命令启动:$ wget https://arthas.aliyun.com/math-game.jar
$ java -jar math-game.jar
math-game
是一个很简单的程序,它随机生成整数,再执行因式分解,把结果打印出来。如果生成的随机数是负数,则会打印提示信息。下载并启动 Arthas 工具
下载arthas-boot.jar
,再用java -jar
命令启动: ```shell $ wget https://arthas.aliyun.com/arthas-boot.jar $ java -jar arthas-boot.jar [INFO] arthas-boot version: 3.5.4 [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
- [1]: 89 math-game.jar
`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)
<a name="tHMYr"></a>
## Dashboard 命令
可以查看当前系统的实时数据面板,输入 `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)
<a name="xgonq"></a>
## Thread 命令
查看当前线程信息,查看线程的堆栈。
```shell
$ 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:
[arthas@89]$ thread 1
"main" Id=1 TIMED_WAITING
at java.base@15-ea/java.lang.Thread.sleep(Native Method)
at java.base@15-ea/java.lang.Thread.sleep(Thread.java:337)
at java.base@15-ea/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at app//demo.MathGame.main(MathGame.java:17)
[arthas@89]$ thread 1 | grep 'main('
at app//demo.MathGame.main(MathGame.java:17)
Sc 命令
“Search-Class” 的简写,查看JVM已加载的类信息。
$ 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里已加载的类,支持通配搜索:
[arthas@89]$ sc *MathGame
demo.MathGame
Affect(row-cnt:1) cost in 9 ms.
通过 -d
参数,可以打印出类加载的具体信息,很方便查找类加载问题:
[arthas@89]$ sc -d *MathGame
class-info demo.MathGame
code-source /root/math-game.jar
name demo.MathGame
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name MathGame
modifier public
annotation
interfaces
super-class +-java.lang.Object
class-loader +-jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@785affc9
classLoaderHash c387f44
Affect(row-cnt:1) cost in 42 ms.
如果搜索的是接口,会找出所有实现类:
[arthas@38]$ sc java.util.List
com.alibaba.arthas.deps.ch.qos.logback.classic.spi.TurboFilterList
com.alibaba.arthas.deps.ch.qos.logback.core.util.COWArrayList
java.util.AbstractList
java.util.AbstractSequentialList
java.util.ArrayList
java.util.ArrayList$SubList
java.util.Arrays$ArrayList
java.util.Collections$EmptyList
java.util.Collections$SingletonList
java.util.Collections$UnmodifiableList
java.util.Collections$UnmodifiableRandomAccessList
java.util.ImmutableCollections$AbstractImmutableList
java.util.ImmutableCollections$List12
java.util.ImmutableCollections$ListN
java.util.ImmutableCollections$SubList
java.util.LinkedList
java.util.List
java.util.Stack
java.util.Vector
java.util.concurrent.CopyOnWriteArrayList
org.benf.cfr.reader.bytecode.analysis.types.StackTypes
sun.security.jca.ProviderList$3
sun.security.jca.ProviderList$ServiceList
Affect(row-cnt:23) cost in 20 ms.
Sm 命令
“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息,只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。
$ 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
命令可以查找类的具体函数:
[arthas@38]$ sm java.math.RoundingMode
java.math.RoundingMode <init>(Ljava/lang/String;II)V
java.math.RoundingMode values()[Ljava/math/RoundingMode;
java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode;
java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode;
Affect(row-cnt:4) cost in 29 ms.
通过 -d
参数可以打印函数的具体属性:
[arthas@38]$ sm -d java.math.RoundingMode
declaring-class java.math.RoundingMode
constructor-name <init>
modifier private
annotation
parameters java.lang.String
int
int
exceptions
classLoaderHash null
declaring-class java.math.RoundingMode
method-name values
modifier public,static
annotation
parameters
return java.math.RoundingMode[]
exceptions
classLoaderHash null
declaring-class java.math.RoundingMode
method-name valueOf
modifier public,static
annotation
parameters int
return java.math.RoundingMode
exceptions
classLoaderHash null
declaring-class java.math.RoundingMode
method-name valueOf
modifier public,static
annotation
parameters java.lang.String
return java.math.RoundingMode
exceptions
classLoaderHash null
Affect(row-cnt:4) cost in 25 ms.
也可以通过函数名称查找指定的函数:
[arthas@38]$ sm java.math.RoundingMode <init>
java.math.RoundingMode <init>(Ljava/lang/String;II)V
Affect(row-cnt:1) cost in 15 ms.
[arthas@38]$ sm java.math.RoundingMode values
java.math.RoundingMode values()[Ljava/math/RoundingMode;
Affect(row-cnt:1) cost in 19 ms.
[arthas@38]$ sm java.math.RoundingMode valueOf
java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode;
java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode;
Affect(row-cnt:2) cost in 17 ms.
Jad 命令
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑,在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便,当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解。
$ 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
命令来反编译代码:
[arthas@89]$ jad demo.MathGame
ClassLoader:
+-jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@785affc9
Location:
/root/math-game.jar
/*
* Decompiled with CFR.
*/
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
...
通过 --source-only
参数实现只打印源代码,不打印其它信息:
[arthas@89]$ jad --source-only demo.MathGame
/*
* Decompiled with CFR.
*/
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class MathGame {
private static Random random = new Random();
private int illegalArgumentCount = 0;
public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
...
Watch 命令
让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL
表达式进行对应变量的查看。
$ 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
命令可以查看函数的参数/返回值/异常信息:
[arthas@89]$ watch demo.MathGame primeFactors returnObj
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 115 ms, listenerId: 1
method=demo.MathGame.primeFactors location=AtExit
ts=2021-09-16 06:30:20; [cost=0.805757ms] result=@ArrayList[
@Integer[2],
@Integer[61],
@Integer[191],
]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2021-09-16 06:30:21; [cost=0.158301ms] result=null
method=demo.MathGame.primeFactors location=AtExit
ts=2021-09-16 06:30:22; [cost=9.142493ms] result=@ArrayList[
@Integer[207169],
]
method=demo.MathGame.primeFactors location=AtExit
ts=2021-09-16 06:30:23; [cost=1.599325ms] result=@ArrayList[
@Integer[107053],
]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2021-09-16 06:30:24; [cost=0.105091ms] result=null
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2021-09-16 06:30:25; [cost=0.069531ms] result=null
method=demo.MathGame.primeFactors location=AtExit
ts=2021-09-16 06:30:26; [cost=0.155641ms] result=@ArrayList[
@Integer[19],
@Integer[3637],
]
Vmtool 命令
vmtool
利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能。
$ vmtool -a {forceGc, getInstances} [-c <value>] [--classLoaderClass <value>] [--className <value>] [-x <value>] [--express <value>] [-h] [--libPath <value>] [-l <value>]
使用示例
搜索内存对象:
[arthas@89]$ vmtool --action getInstances --className java.lang.String --limit 10
@String[][
@String[vmtool],
@String[vmtool],
@String[ ],
@String[--action],
@String[--action],
@String[ ],
@String[getInstances],
@String[getInstances],
@String[ ],
@String[vmtool --action getInstances --className java.lang.String --limit 10],
]
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
$ wget https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
$ java -jar demo-arthas-spring-boot.jar
Sysprop 命令
sysprop
命令可以用于查看当前JVM的系统属性(System Property)。
$ sysprop [-h] [property-name] [property-value]
使用示例
查看当前JVM的系统属性(System Property):
[arthas@38]$ sysprop
KEY VALUE
----------------------------------------------------------------------------------------------------
java.specification 15
.version
sun.jnu.encoding UTF-8
java.class.path demo-arthas-spring-boot.jar
java.vm.vendor Oracle Corporation
sun.arch.data.mode 64
l
java.vendor.url https://java.oracle.com/
catalina.useNaming false
user.timezone Etc/UTC
org.jboss.logging. slf4j
provider
java.vm.specificat 15
ion.version
os.name Linux
sun.java.launcher SUN_STANDARD
user.country US
sun.boot.library.p /usr/java/openjdk-15/lib
ath
...
指定显示单个 key:
[arthas@38]$ sysprop java.version
KEY VALUE
----------------------------------------------------------------------------------------------------
java.version 15-ea
支持通过 grep 过滤:
[arthas@38]$ sysprop | grep java
java.specification 15
java.class.path demo-arthas-spring-boot.jar
java.vm.vendor Oracle Corporation
java.vendor.url https://java.oracle.com/
java.vm.specificat 15
sun.java.launcher SUN_STANDARD
sun.boot.library.p /usr/java/openjdk-15/lib
sun.java.command demo-arthas-spring-boot.jar
java.specification Oracle Corporation
java.version.date 2020-09-15
java.home /usr/java/openjdk-15
java.vm.compressed 32-bit
java.specification Java Platform API Specification
java.vm.specificat Oracle Corporation
java.awt.headless true
java.protocol.hand org.springframework.boot.loader
java.runtime.versi 15-ea+8-219
java.runtime.name OpenJDK Runtime Environment
java.vm.name OpenJDK 64-Bit Server VM
...
设置自定义的 value:
[arthas@38]$ sysprop testKey testValue
Successfully changed the system property.
KEY VALUE
----------------------------------------------------------------------------------------------------
testKey testValue
Sysenv 命令
sysenv
命令可以用于查看当前JVM的环境属性(System Environment Variables)。
$ sysenv [-h] [env-name]
使用示例
sysenv
可以获取环境变量:
[arthas@38]$ sysenv
KEY VALUE
----------------------------------------------------------------------------------------------------
PATH /usr/java/openjdk-15/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
in:/bin:/usr/bin/gradle/bin:/usr/local/bin/scala-2.13.2/bin:/usr/local/bin/sbt
/bin
LESSCLOSE /usr/bin/lesspipe %s %s
SBT_VERSION 1.3.0
JAVA_HOME /usr/java/openjdk-15
TERM xterm
LANG C.UTF-8
PS1 $
GRADLE_HOME /usr/bin/gradle
MAVEN_HOME /usr/share/maven
GRADLE_USER_HOME /cache
JAVA_SHA256 ab41dd4e616e015ec94f26e4d2c8253f376f5f5d574db811f341abc6c55e333c
SCALA_VERSION 2.13.2
JAVA_VERSION 15-ea+8
...
Jvm 命令
jvm
命令可以用于查看 jvm 的信息。
$ jvm [-h]
使用示例
jvm
可以打印 jvm 的各种详细信息:
[arthas@38]$ jvm
RUNTIME
----------------------------------------------------------------------------------------------------
MACHINE-NAME 38@4ba353efdd4e
JVM-START-TIME 2021-09-16 06:46:24
MANAGEMENT-SPEC-VERSION 3.0
SPEC-NAME Java Virtual Machine Specification
SPEC-VENDOR Oracle Corporation
SPEC-VERSION 15
VM-NAME OpenJDK 64-Bit Server VM
VM-VENDOR Oracle Corporation
VM-VERSION 15-ea+8-219
INPUT-ARGUMENTS []
CLASS-PATH demo-arthas-spring-boot.jar
BOOT-CLASS-PATH
LIBRARY-PATH /usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib
...
Ognl 命令
执行ognl表达式。
$ ognl [-c <value>] [--classLoaderClass <value>] [-x <value>] [-h] express
参数说明
express | 执行的表达式 |
---|---|
[c:] | 执行表达式的 ClassLoader 的 hashcode,默认值是SystemClassLoader |
[classLoaderClass:] | 指定执行表达式的 ClassLoader 的 class name |
[x] | 结果对象的展开层次,默认值1 |
使用示例
调用静态方法
[arthas@39]$ ognl '@java.lang.Math@random()'
@Double[0.36922744380446826]
获取类的静态字段
方法1:使用 -c
参数指定 ClassLoader hashcode
使用 sc 命令获取类的 ClassLoader hashcode:
[arthas@8833]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
classLoaderHash 5b2133b1
使用 ognl -c <class loader hashcode> <express>
获取类的静态字段:
[arthas@8833]$ ognl -c 5b2133b1 @com.example.demo.arthas.user.UserController@logger
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.example.demo.arthas.user.UserController],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[Logger[com.example.demo.arthas.user]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]
对于只有唯一实例的 ClassLoader 可以通过 --classLoaderClass
指定 class name,使用起来更加方便:
[arthas@8833]$ ognl -classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader @org.springframework.boot.SpringApplication@logger
serialVersionUID=@Long[-2379157579039314822],
name=@String[org.springframework.boot.SpringApplication],
logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
FQCN=@String[org.apache.commons.logging.impl.SLF4JLocationAwareLog],
]
还可以通过 -x
参数控制返回值的展开层数。比如:
[arthas@8833]$ ognl -classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -x 2 @@SLF4JLocationAwareLog[t.SpringApplication@logger
serialVersionUID=@Long[-2379157579039314822],
name=@String[org.springframework.boot.SpringApplication],
logger=@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[org.springframework.boot.SpringApplication],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[Logger[org.springframework.boot]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
],
FQCN=@String[org.apache.commons.logging.impl.SLF4JLocationAwareLog],
]
案例
排查函数调用异常
现象
访问 http://localhost/user/0 会返回异常:
Something went wrong: 500 Internal Server Error
通过 watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
命令监控方法的参数、异常信息。
- 第一个参数是类名,支持通配
- 第二个参数是函数名,支持通配
这时再访问 http://localhost/user/0:
[arthas@8833]$ watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 190 ms, listenerId: 1
method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
ts=2021-09-18 13:48:44; [cost=4.921205ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@IllegalArgumentException[java.lang.IllegalArgumentException: id < 1],
]
发现抛出的是 IllegalArgumentException。
返回值表达式
上面的例子中,第三个参数是返回值表达式
,它实际上是一个 ognl
表达式,它支持一些内置对象:
- loader
- clazz
- method
- target
- params
- returnObj
- throwExp
- isBefore
- isThrow
- isReturn
可以利用这些内置对象来组成不同的表达式。比如返回一个数组:
[arthas@8833]$ watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 119 ms, listenerId: 2
method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
ts=2021-09-18 14:27:20; [cost=1.548078ms] result=@ArrayList[
@Integer[0],
@UserController[com.example.demo.arthas.user.UserController@5d720b2f],
null,
]
method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
ts=2021-09-18 14:27:22; [cost=1.069647ms] result=@ArrayList[
@Integer[1],
@UserController[com.example.demo.arthas.user.UserController@5d720b2f],
@User[com.example.demo.arthas.user.User@2457a06b],
]
条件表达式
watch 命令支持在第 4 个参数里写条件表达式,比如:
[arthas@8833]$ watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 31 ms, listenerId: 3
method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
ts=2021-09-18 14:31:44; [cost=0.330385ms] result=@User[
id=@Integer[101],
name=@String[name101],
]
当访问 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异常:
Something went wrong: 500 Internal Server Error
jad 反编译 UserController
jad
反编译的结果保存在 /tmp/UserController.java
文件里:
[arthas@8833]$ jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
编辑文件内容,当 id 小于 1 时,不抛出异常:
@GetMapping(value={"/user/{id}"})
public User findUserById(@PathVariable Integer id) {
logger.info("id: {}", (Object)id);
if (id != null && id < 1) {
// throw new IllegalArgumentException("id < 1");
}
return new User(id.intValue(), "name" + id);
}
sc 查找加载 UserController 的 ClassLoader
[arthas@8833]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
classLoaderHash 5b2133b1
mc 编译
使用 mc(Memory Compiler)
命令来编译,并且通过 -c
或者 --classLoaderClass
参数指定ClassLoader:
[arthas@8833]$ mc -c 5b2133b1 /tmp/UserController.java -d /tmp
Memory compiler output:
/tmp/com/example/demo/arthas/user/UserController.class
Affect(row-cnt:1) cost in 2271 ms.
redefine 重新加载 class
[arthas@8833]$ redefine /tmp/com/example/demo/arthas/user/UserController.class
redefine success, size: 1, classes:
com.example.demo.arthas.user.UserController
测试
访问 http://localhost/user/0 可以正常返回了:
{"id":0, "name":"name0"}
动态更新应用 Logger Level
现象
UserController 的 logger level 为 null,需要将其进行赋值为 debug。
用 sc 获取 UserController 的 ClassLoader
[arthas@1145]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
classLoaderHash 5b2133b1
用 ognl 获取 logger 的值
[arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger'
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.example.demo.arthas.user.UserController],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[Logger[com.example.demo.arthas.user]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]
可以知道 UserController@logger
实际使用的是 logback。可以看到 level=null
,则说明实际最终的 level 是从 root logger 里来的。
使用 ognl 调用 set 方法进行赋值
[arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
null
再次获取 logger 的值
再次获取 UserController@logger
,可以发现已经是 DEBUG 了:
[arthas@1145]$ ognl -c 5b2133b1 '@com.example.demo.arthas.user.UserController@logger'
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.example.demo.arthas.user.UserController],
level=@Level[DEBUG],
effectiveLevelInt=@Integer[10000],
parent=@Logger[Logger[com.example.demo.arthas.user]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]
修改 logback 的全局 logger level
[arthas@1145]$ ognl -c 5b2133b1 '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
null
获取 logback 的全局 logger level
再次获取 LoggerFactory@getLogger
,可以发现已经是 DEBUG 了:
[arthas@1145]$ ognl -c 5b2133b1 '@org.slf4j.LoggerFactory@getLogger("root")'
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[ROOT],
level=@Level[DEBUG],
effectiveLevelInt=@Integer[10000],
parent=null,
childrenList=@CopyOnWriteArrayList[isEmpty=false;size=3],
aai=@AppenderAttachableImpl[ch.qos.logback.core.spi.AppenderAttachableImpl@9f47a2a],
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]