命令列表

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

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 - 查看已加载类的方法信息

monitor/watch/trace 相关

注意

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令。

  • monitor - 方法执行监控
  • stack - 输出当前方法被调用的调用路径
  • trace - 方法内部调用路径,并输出方法路径上的每个节点上耗时
  • tt - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
  • watch - 方法执行数据观测

profiler/火焰图

鉴权

options

  • options - 查看或设置 Arthas 全局开关

管道

Arthas 支持使用管道对上述命令的结果进行进一步的处理,如sm java.lang.String * | grep 'index'

  • grep - 搜索满足条件的结果
  • plaintext - 将命令的结果去除 ANSI 颜色
  • wc - 按行统计输出结果

后台异步任务

当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考这里

  • 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天)
  • jobs - 列出所有 job
  • kill - 强制终止任务
  • fg - 将暂停的任务拉到前台执行
  • bg - 将暂停的任务放到后台执行

基础命令

  • base64 - base64 编码转换,和 linux 里的 base64 命令类似
  • cat - 打印文件内容,和 linux 里的 cat 命令类似
  • cls - 清空当前屏幕区域
  • echo - 打印参数,和 linux 里的 echo 命令类似
  • grep - 匹配查找,和 linux 里的 grep 命令类似
  • help - 查看命令帮助信息
  • history - 打印命令历史
  • keymap - Arthas 快捷键列表及自定义快捷键
  • pwd - 返回当前的工作目录,和 linux 命令类似
  • quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
  • reset - 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
  • session - 查看当前会话的信息
  • stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出
  • tee - 复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似
  • version - 输出当前目标 Java 进程所加载的 Arthas 版本号

Cat 命令

提示

打印文件内容,和 linux 里的 cat 命令类似。

使用参考:

1
$ cat /tmp/a.txt

Classloader 命令

列出所有 ClassLoader

1
classloader -l

统计 ClassLoader 实际使用 URL 和未使用的 URL

1
classloader --url-stat

注意:基于 JVM 目前已加载的所有类统计,不代表 Unused URLs 可以从应用中删掉。因为可能将来需要从 Unused URLs 里加载类,或者需要加载 resources

列出 ClassLoader 里加载的所有类

列出上面的org.apache.jasper.servlet.JasperLoader 加载的类:

1
classloader -a --classLoaderClass org.apache.jasper.servlet.JasperLoader | grep hello

查看类的 classloader 层次

1
sc -d org.apache.jsp.jsp.hello_jsp

查看 ClassLoader 树

1
classloader -t

查看 URLClassLoader 实际的 urls

比如上面查看到的 spring LaunchedURLClassLoader 为 org.springframework.boot.loader.LaunchedURLClassLoader ,可以通过 -c <hashcode> 参数来指定 classloader,还有一种方法可以通过使用 --classLoaderClass 指定类名,从而查看 URLClassLoader 实际的 urls:

1
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader

查找 ClassLoader 里的资源文件

查找指定的资源文件: classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r logback-spring.xml

也可以尝试查找类的 class 文件:

1
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r java/lang/String.class

Cls 命令

通过 cls 命令 可以清空当前屏幕区域。

1
cls

Grep 命令

提示

类似传统的 grep 命令。

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
USAGE:
grep [-A <value>] [-B <value>] [-C <value>] [-h] [-i] [-v] [-n] [-m <value>] [-e] [--trim-end] pattern

SUMMARY:
grep command for pipes.

EXAMPLES:
sysprop | grep java
sysprop | grep java -n
sysenv | grep -v JAVA
sysenv | grep -e "(?i)(JAVA|sun)" -m 3 -C 2
sysenv | grep JAVA -A2 -B3
thread | grep -m 10 -e "TIMED_WAITING|WAITING"

WIKI:
https://arthas.aliyun.com/doc/grep

OPTIONS:
-A, --after-context <value> Print NUM lines of trailing context)
-B, --before-context <value> Print NUM lines of leading context)
-C, --context <value> Print NUM lines of output context)
-h, --help this help
-i, --ignore-case Perform case insensitive matching. By default, grep is case sensitive.
-v, --invert-match Select non-matching lines
-n, --line-number Print line number with output lines
-m, --max-count <value> stop after NUM selected lines)
-e, --regex Enable regular expression to match
--trim-end Remove whitespaces at the end of the line
<pattern> Pattern

匹配展示符合范本样式的项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[arthas@13372]$ sysprop | grep java
java.specification.version 1.8
java.class.path math-game.jar
java.vm.vendor Oracle Corporation
java.vendor.url http://java.oracle.com/
java.vm.specification.version 1.8
sun.java.launcher SUN_STANDARD
sun.java.command math-game.jar
java.specification.vendor Oracle Corporation
java.home D:\developInstall\JDK\jdk1.8.0_161\jre
java.vm.specification.vendor Oracle Corporation
java.specification.name Java Platform API Specification
java.awt.graphicsenv sun.awt.Win32GraphicsEnvironment
java.runtime.version 1.8.0_161-b12
java.endorsed.dirs D:\developInstall\JDK\jdk1.8.0_161\jre\lib\endorsed
java.runtime.name Java(TM) SE Runtime Environment
java.vm.name Java HotSpot(TM) 64-Bit Server VM
java.vendor.url.bug http://bugreport.sun.com/bugreport/
java.io.tmpdir C:\Users\PT-TIA~1\AppData\Local\Temp\
java.version 1.8.0_161
java.vm.specification.name Java Virtual Machine Specification
java.awt.printerjob sun.awt.windows.WPrinterJob
java.library.path D:\developInstall\JDK\jdk1.8.0_161\bin;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;C:\Python27\;C:\Python27\Scripts;D:\app\pt-tianzhencai\product\11.1.0\db_1\bin;C:\Program Files (x86)\Comm
on Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\TortoiseSVN\bin;C:\Progr
java.vm.info mixed mode
java.vendor Oracle Corporation
java.vm.version 25.161-b12
java.ext.dirs D:\developInstall\JDK\jdk1.8.0_161\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
java.class.version 52.0

-n 命令显示行号:

1
sysprop | grep java -n

-v 展示非匹配

1
sysenv | grep -v JAVA

-e 使用正则表达式匹配,-m 设定最大展示条数,

1
2
sysenv | grep -e "(?i)(JAVA|sun)" -m 3 -C 2
thread | grep -m 10 -e "TIMED_WAITING|WAITING"

除了显示符合范本样式的那一行之外,-A 指定显示该行之后的内容,-B 指定显示该行之前的内容。

1
sysenv | grep JAVA -A2 -B3

Dashboard 命令

dashboard 命令可以查看当前系统的实时数据面板。

当运行在 Ali-tomcat 时,会显示当前 tomcat 的实时信息,如 HTTP 请求的 qps, rt, 错误数, 线程池信息等等。

参数说明

参数名称 参数说明
[i:] 刷新实时数据的时间间隔 (ms),默认 5000ms
[n:] 刷新实时数据的次数

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ dashboard
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
-1 C2 CompilerThread0 - -1 - 1.55 0.077 0:8.684 false true
53 Timer-for-arthas-dashboard-07b system 5 RUNNABLE 0.08 0.004 0:0.004 false true
22 scheduling-1 main 5 TIMED_WAI 0.06 0.003 0:0.287 false false
-1 C1 CompilerThread0 - -1 - 0.06 0.003 0:2.171 false true
-1 VM Periodic Task Thread - -1 - 0.03 0.001 0:0.092 false true
49 arthas-NettyHttpTelnetBootstra system 5 RUNNABLE 0.02 0.001 0:0.156 false true
16 Catalina-utility-1 main 1 TIMED_WAI 0.0 0.000 0:0.029 false false
-1 G1 Young RemSet Sampling - -1 - 0.0 0.000 0:0.019 false true
17 Catalina-utility-2 main 1 WAITING 0.0 0.000 0:0.025 false false
34 http-nio-8080-ClientPoller main 5 RUNNABLE 0.0 0.000 0:0.016 false true
23 http-nio-8080-BlockPoller main 5 RUNNABLE 0.0 0.000 0:0.011 false true
-1 VM Thread - -1 - 0.0 0.000 0:0.032 false true
-1 Service Thread - -1 - 0.0 0.000 0:0.006 false true
-1 GC Thread#5 - -1 - 0.0 0.000 0:0.043 false true
Memory used total max usage GC
heap 36M 70M 4096M 0.90% gc.g1_young_generation.count 12
g1_eden_space 6M 18M -1 33.33% 86
g1_old_gen 30M 50M 4096M 0.74% gc.g1_old_generation.count 0
g1_survivor_space 491K 2048K -1 24.01% gc.g1_old_generation.time(ms) 0
nonheap 66M 69M -1 96.56%
codeheap_'non-nmethods' 1M 2M 5M 22.39%
metaspace 46M 47M -1 98.01%
Runtime
os.name Mac OS X
os.version 10.15.4
java.version 15
java.home /Library/Java/JavaVirtualMachines/jdk-15.jdk/Contents/Home
systemload.average 10.68
processors 8
uptime 272s

数据说明

  • 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 线程

JVM 内部线程

Java 8 之后支持获取 JVM 内部线程 CPU 时间,这些线程只有名称和 CPU 时间,没有 ID 及状态等信息(显示 ID 为-1)。 通过内部线程可以观测到 JVM 活动,如 GC、JIT 编译等占用 CPU 情况,方便了解 JVM 整体运行状况。

  • 当 JVM 堆(heap)/元数据(metaspace)空间不足或 OOM 时,可以看到 GC 线程的 CPU 占用率明显高于其他的线程。
  • 当执行trace/watch/tt/redefine等命令后,可以看到 JIT 线程活动变得更频繁。因为 JVM 热更新 class 字节码时清除了此 class 相关的 JIT 编译结果,需要重新编译。

JVM 内部线程包括下面几种:

  • JIT 编译线程: 如 C1 CompilerThread0, C2 CompilerThread0
  • GC 线程: 如GC Thread0, G1 Young RemSet Sampling
  • 其它内部线程: 如VM Periodic Task Thread, VM Thread, Service Thread

dump 命令

提示

dump 已加载类的 bytecode 到特定目录

dump 命令将 JVM 中实际运行的 class 的 byte code dump 到指定目录,适用场景批量下载指定包目录的 class 字节码;如需反编译单一类、实时查看类信息,可参考 jad

参数说明

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

使用参考

1
2
3
4
$ dump java.lang.String
HASHCODE CLASSLOADER LOCATION
null /Users/admin/logs/arthas/classdump/java/lang/String.class
Affect(row-cnt:1) cost in 119 ms.
1
2
3
4
5
$ dump demo.*
HASHCODE CLASSLOADER LOCATION
3d4eac69 +-sun.misc.Launcher$AppClassLoader@3d4eac69 /Users/admin/logs/arthas/classdump/sun.misc.Launcher$AppClassLoader-3d4eac69/demo/MathGame.class
+-sun.misc.Launcher$ExtClassLoader@66350f69
Affect(row-cnt:1) cost in 39 ms.
1
2
3
4
$ dump -d /tmp/output java.lang.String
HASHCODE CLASSLOADER LOCATION
null /tmp/output/java/lang/String.class
Affect(row-cnt:1) cost in 138 ms.
  • 指定 classLoader

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。

如果你使用-c,你需要手动输入 hashcode:-c <hashcode>

1
$ dump -c 3d4eac69 demo.*

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

1
2
3
4
5
$ dump --classLoaderClass sun.misc.Launcher$AppClassLoader demo.*
HASHCODE CLASSLOADER LOCATION
3d4eac69 +-sun.misc.Launcher$AppClassLoader@3d4eac69 /Users/admin/logs/arthas/classdump/sun.misc.Launcher$AppClassLoader-3d4eac69/demo/MathGame.class
+-sun.misc.Launcher$ExtClassLoader@66350f69
Affect(row-cnt:1) cost in 39 ms.
  • 注:这里 classLoaderClass 在 java 8 是 sun.misc.Launcher$AppClassLoader,而 java 11 的 classloader 是 jdk.internal.loader.ClassLoaders$AppClassLoader,killercoda 目前环境是 java11。

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。


echo 命令

提示

打印参数,和 linux 里的 echo 命令类似。

使用参考

1
$ echo 'hello'

Getstatic 命令

  • 推荐直接使用ognl命令,更加灵活。

通过 getstatic 命令可以方便的查看类的静态属性。使用方法为getstatic class_name field_name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ getstatic demo.MathGame random
field: random
@Random[
serialVersionUID=@Long[3905348978240129619],
seed=@AtomicLong[120955813885284],
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@2eaa1027],
seedOffset=@Long[24],
]
  • 指定 classLoader

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,使用sc -d <ClassName>提取对应 ClassLoader 的 hashcode。

如果你使用-c,你需要手动输入 hashcode:-c <hashcode>

1
$ getstatic -c 3d4eac69 demo.MathGame random

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

1
getstatic --classLoaderClass sun.misc.Launcher$AppClassLoader demo.MathGame random
  • 注: 这里 classLoaderClass 在 java 8 是 sun.misc.Launcher$AppClassLoader,而java 11的classloader是jdk.internal.loader.ClassLoaders$AppClassLoader,killercoda 目前环境是 java11。

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。


Help 命令

查看命令帮助信息,可以查看当前 arthas 版本支持的指令,或者查看具体指令的使用说明。

提示

[help 指令]的等同于[指令 -help],都是查看具体指令的使用说明。

参数说明

参数名称 参数说明
不接参数 查询当前 arthas 版本支持的指令以及指令描述
[name:] 查询具体指令的使用说明

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ help dashboard
USAGE:
dashboard [-h] [-i <value>] [-n <value>]

SUMMARY:
Overview of target jvm's thread, memory, gc, vm, tomcat info.

EXAMPLES:
dashboard
dashboard -n 10
dashboard -i 2000

WIKI:
https://arthas.aliyun.com/doc/dashboard

OPTIONS:
-h, --help this help
-i, --interval <value> The interval (in ms) between two executions, default is 5000 ms.
-n, --number-of-execution <value> The number of times this command will be executed.

history 命令

打印命令历史。

提示

历史指令会通过一个名叫 history 的文件持久化,所以 history 指令可以查看当前 arthas 服务器的所有历史命令,而不仅只是当前次会话使用过的命令。

参数说明

参数名称 参数说明
[c:] 清空历史指令
[n:] 显示最近执行的 n 条指令

使用参考

1
2
3
4
5
#查看最近执行的3条指令
$ history 3
269 thread
270 cls
271 history 3
1
2
3
4
#清空指令
$ history -c
$ history 3
1 history 3

Jad 命令

jad 命令 将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;

  • 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
  • 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解

参数说明

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

使用参考

反编译java.lang.String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ jad java.lang.String

ClassLoader:

Location:


/*
* Decompiled with CFR.
*/
package java.lang;

import java.io.ObjectStreamField;
import java.io.Serializable;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
...
public String(byte[] byArray, int n, int n2, Charset charset) {
/*460*/ if (charset == null) {
throw new NullPointerException("charset");
}
/*462*/ String.checkBounds(byArray, n, n2);
/*463*/ this.value = StringCoding.decode(charset, byArray, n, n2);
}
...

反编译时只显示源代码

默认情况下,反编译结果里会带有ClassLoader信息,通过--source-only选项,可以只打印源代码。方便和mc/retransform命令结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ jad --source-only demo.MathGame
/*
* Decompiled with CFR 0_132.
*/
package demo;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
private static Random random = new Random();
public int illegalArgumentCount = 0;
...

反编译指定的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ jad demo.MathGame main

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@232204a1
+-sun.misc.Launcher$ExtClassLoader@7f31245a

Location:
/private/tmp/math-game.jar

public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}

反编译时不显示行号

--lineNumber 参数默认值为 true,显示指定为 false 则不打印行号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ jad demo.MathGame main --lineNumber false

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@232204a1
+-sun.misc.Launcher$ExtClassLoader@7f31245a

Location:
/private/tmp/math-game.jar

public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
game.run();
TimeUnit.SECONDS.sleep(1L);
}
}

反编译时指定 ClassLoader

提示

当有多个 ClassLoader 都加载了这个类时,jad 命令会输出对应 ClassLoader 实例的 hashcode,然后你只需要重新执行 jad 命令,并使用参数 -c <hashcode> 就可以反编译指定 ClassLoader 加载的那个类了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ jad org.apache.log4j.Logger

Found more than one class for: org.apache.log4j.Logger, Please use jad -c hashcode org.apache.log4j.Logger
HASHCODE CLASSLOADER
69dcaba4 +-monitor's ModuleClassLoader
6e51ad67 +-java.net.URLClassLoader@6e51ad67
+-sun.misc.Launcher$AppClassLoader@6951a712
+-sun.misc.Launcher$ExtClassLoader@6fafc4c2
2bdd9114 +-pandora-qos-service's ModuleClassLoader
4c0df5f8 +-pandora-framework's ModuleClassLoader

Affect(row-cnt:0) cost in 38 ms.
$ jad org.apache.log4j.Logger -c 69dcaba4

ClassLoader:
+-monitor's ModuleClassLoader

Location:
/Users/admin/app/log4j-1.2.14.jar

package org.apache.log4j;

import org.apache.log4j.spi.*;

public class Logger extends Category
{
private static final String FQCN;

protected Logger(String name)
{
super(name);
}

...

Affect(row-cnt:1) cost in 190 ms.

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

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。

jfr 命令

提示

Java Flight Recorder (JFR) 是一种用于收集有关正在运行的 Java 应用程序的诊断和分析数据的工具。它集成到 Java 虚拟机 (JVM) 中,几乎不会造成性能开销,因此即使在负载较重的生产环境中也可以使用。

jfr 命令支持在程序动态运行过程中开启和关闭 JFR 记录。 记录收集有关 event 的数据。事件在特定时间点发生在 JVM 或 Java 应用程序中。每个事件都有一个名称、一个时间戳和一个可选的有效负载。负载是与事件相关的数据,例如 CPU 使用率、事件前后的 Java 堆大小、锁持有者的线程 ID 等。

1
jfr 命令基本运行结构是 jfr cmd [actionArg]

注意: JDK8 的 8u262 版本之后才支持 jfr

参数说明

参数名称 参数说明
cmd 要执行的操作 支持的命令【start,status,dump,stop】
actionArg 属性名模式
[n:] 记录名称
[r:] 记录 id 值
[dumponexit:] 程序退出时,是否要 dump 出 .jfr 文件,默认为 false
[d:] 延迟多久后启动 JFR 记录,支持带单位配置,eg: 60s, 2m, 5h, 3d. 不带单位就是秒,默认无延迟
[duration:] JFR 记录持续时间,支持单位配置,不带单位就是秒,默认一直记录
[s:] 采集 Event 的详细配置文件,默认是 default.jfc 位于 $JAVA_HOME/lib/jfr/default.jfc
[f:] 将输出转储到指定路径
[maxage:] 缓冲区数据最大文件记录保存时间,支持单位配置,不带单位就是秒,默认是不限制
[maxsize:] 缓冲区的最大文件大小,支持单位配置, 不带单位是字节,m 或者 M 代表 MB,g 或者 G 代表 GB。
[state:] jfr 记录状态

启动 JFR 记录

1
2
$ jfr start
Started recording 1. No limit specified, using maxsize=250MB as default.

提示

默认情况下,开启的是默认参数的 jfr 记录

启动 jfr 记录,指定记录名,记录持续时间,记录文件保存路径。

1
2
3
$ jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr
Started recording 2. The result will be written to:
/tmp/myRecording.jfr

查看 JFR 记录状态

默认是查看所有 JFR 记录信息

1
2
3
$ jfr status
Recording: recording=1 name=Recording-1 (running)
Recording: recording=2 name=myRecording duration=PT1M (closed)

查看指定记录 id 的记录信息

1
2
$ jfr status -r 1
Recording: recording=1 name=Recording-1 (running)

查看指定状态的记录信息

1
2
$ jfr status --state closed
Recording: recording=2 name=myRecording duration=PT1M (closed)

dump jfr 记录

jfr dump 会输出从开始到运行该命令这段时间内的记录到 JFR 文件,且不会停止 jfr 的记录
指定记录输出路径

1
2
3
$ jfr dump -r 1 -f /tmp/myRecording1.jfr
Dump recording 1, The result will be written to:
/tmp/myRecording1.jfr

不指定文件输出路径,默认是保存到arthas-output目录下

1
2
3
$ jfr dump -r 1
Dump recording 1, The result will be written to:
/tmp/test/arthas-output/20220819-200915.jfr

停止 jfr 记录

不指定记录输出路径,默认是保存到arthas-output目录下

1
2
3
$ jfr stop -r 1
Stop recording 1, The result will be written to:
/tmp/test/arthas-output/20220819-202049.jfr

注意一条记录只能停止一次。

也可以指定记录输出路径。

通过浏览器查看 arthas-output 下面 JFR 记录的结果

默认情况下,arthas 使用 8563 端口,则可以打开: http://localhost:8563/arthas-output/在新窗口打开 查看到arthas-output目录下面的 JFR 记录结果:

img

生成的结果可以用支持 jfr 格式的工具来查看。比如:


Jvm 命令

提示

查看当前 JVM 信息

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
$ jvm
RUNTIME
--------------------------------------------------------------------------------------------------------------
MACHINE-NAME 37@ff267334bb65
JVM-START-TIME 2020-07-23 07:50:36
MANAGEMENT-SPEC-VERSION 1.2
SPEC-NAME Java Virtual Machine Specification
SPEC-VENDOR Oracle Corporation
SPEC-VERSION 1.8
VM-NAME Java HotSpot(TM) 64-Bit Server VM
VM-VENDOR Oracle Corporation
VM-VERSION 25.201-b09
INPUT-ARGUMENTS []
CLASS-PATH demo-arthas-spring-boot.jar
BOOT-CLASS-PATH /usr/lib/jvm/java-8-oracle/jre/lib/resources.jar:/usr/lib/jvm/java-8-oracle/j
re/lib/rt.jar:/usr/lib/jvm/java-8-oracle/jre/lib/sunrsasign.jar:/usr/lib/jvm/
java-8-oracle/jre/lib/jsse.jar:/usr/lib/jvm/java-8-oracle/jre/lib/jce.jar:/us
r/lib/jvm/java-8-oracle/jre/lib/charsets.jar:/usr/lib/jvm/java-8-oracle/jre/l
ib/jfr.jar:/usr/lib/jvm/java-8-oracle/jre/classes
LIBRARY-PATH /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib

--------------------------------------------------------------------------------------------------------------
CLASS-LOADING
--------------------------------------------------------------------------------------------------------------
LOADED-CLASS-COUNT 7529
TOTAL-LOADED-CLASS-COUNT 7529
UNLOADED-CLASS-COUNT 0
IS-VERBOSE false

--------------------------------------------------------------------------------------------------------------
COMPILATION
--------------------------------------------------------------------------------------------------------------
NAME HotSpot 64-Bit Tiered Compilers
TOTAL-COMPILE-TIME 14921(ms)

--------------------------------------------------------------------------------------------------------------
GARBAGE-COLLECTORS
--------------------------------------------------------------------------------------------------------------
PS Scavenge name : PS Scavenge
[count/time (ms)] collectionCount : 7
collectionTime : 68

PS MarkSweep name : PS MarkSweep
[count/time (ms)] collectionCount : 1
collectionTime : 47

--------------------------------------------------------------------------------------------------------------
MEMORY-MANAGERS
--------------------------------------------------------------------------------------------------------------
CodeCacheManager Code Cache

Metaspace Manager Metaspace
Compressed Class Space

Copy Eden Space
Survivor Space

MarkSweepCompact Eden Space
Survivor Space
Tenured Gen


--------------------------------------------------------------------------------------------------------------
MEMORY
--------------------------------------------------------------------------------------------------------------
HEAP-MEMORY-USAGE init : 268435456(256.0 MiB)
[memory in bytes] used : 18039504(17.2 MiB)
committed : 181403648(173.0 MiB)
max : 3817865216(3.6 GiB)

NO-HEAP-MEMORY-USAGE init : 2555904(2.4 MiB)
[memory in bytes] used : 33926216(32.4 MiB)
committed : 35176448(33.5 MiB)
max : -1(-1 B)

--------------------------------------------------------------------------------------------------------------
OPERATING-SYSTEM
--------------------------------------------------------------------------------------------------------------
OS Linux
ARCH amd64
PROCESSORS-COUNT 3
LOAD-AVERAGE 29.53
VERSION 4.15.0-52-generic

--------------------------------------------------------------------------------------------------------------
THREAD
--------------------------------------------------------------------------------------------------------------
COUNT 30
DAEMON-COUNT 24
PEAK-COUNT 31
STARTED-COUNT 36
DEADLOCK-COUNT 0

--------------------------------------------------------------------------------------------------------------
FILE-DESCRIPTOR
--------------------------------------------------------------------------------------------------------------
MAX-FILE-DESCRIPTOR-COUNT 1048576
OPEN-FILE-DESCRIPTOR-COUNT 100
Affect(row-cnt:0) cost in 88 ms.

THREAD 相关

  • COUNT: JVM 当前活跃的线程数
  • DAEMON-COUNT: JVM 当前活跃的守护线程数
  • PEAK-COUNT: 从 JVM 启动开始曾经活着的最大线程数
  • STARTED-COUNT: 从 JVM 启动开始总共启动过的线程次数
  • DEADLOCK-COUNT: JVM 当前死锁的线程数

文件描述符相关

  • MAX-FILE-DESCRIPTOR-COUNT:JVM 进程最大可以打开的文件描述符数
  • OPEN-FILE-DESCRIPTOR-COUNT:JVM 当前打开的文件描述符数

Keymap 命令

keymap命令输出当前的快捷键映射表:

默认的快捷键如下:

快捷键 快捷键说明 命令名称 命令说明
"\C-a" ctrl + a beginning-of-line 跳到行首
"\C-e" ctrl + e end-of-line 跳到行尾
"\C-f" ctrl + f forward-word 向前移动一个单词
"\C-b" ctrl + b backward-word 向后移动一个单词
"\e[D" 键盘左方向键 backward-char 光标向前移动一个字符
"\e[C" 键盘右方向键 forward-char 光标向后移动一个字符
"\e[B" 键盘下方向键 next-history 下翻显示下一个命令
"\e[A" 键盘上方向键 previous-history 上翻显示上一个命令
"\C-h" ctrl + h backward-delete-char 向后删除一个字符
"\C-?" ctrl + shift + / backward-delete-char 向后删除一个字符
"\C-u" ctrl + u undo 撤销上一个命令,相当于清空当前行
"\C-d" ctrl + d delete-char 删除当前光标所在字符
"\C-k" ctrl + k kill-line 删除当前光标到行尾的所有字符
"\C-i" ctrl + i complete 自动补全,相当于敲TAB
"\C-j" ctrl + j accept-line 结束当前行,相当于敲回车
"\C-m" ctrl + m accept-line 结束当前行,相当于敲回车
"\C-w" backward-delete-word
"\C-x\e[3~" backward-kill-line
"\e\C-?" backward-kill-word
  • 任何时候 tab 键,会根据当前的输入给出提示
  • 命令后敲 --- ,然后按 tab 键,可以展示出此命令具体的选项

自定义快捷键

在当前用户目录下新建$USER_HOME/.arthas/conf/inputrc文件,加入自定义配置。

假设我是 vim 的重度用户,我要把ctrl+h设置为光标向前一个字符,则设置如下,首先拷贝默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"\C-a": beginning-of-line
"\C-e": end-of-line
"\C-f": forward-word
"\C-b": backward-word
"\e[D": backward-char
"\e[C": forward-char
"\e[B": next-history
"\e[A": previous-history
"\C-h": backward-delete-char
"\C-?": backward-delete-char
"\C-u": undo
"\C-d": delete-char
"\C-k": kill-line
"\C-i": complete
"\C-j": accept-line
"\C-m": accept-line
"\C-w": backward-delete-word
"\C-x\e[3~": backward-kill-line
"\e\C-?": backward-kill-word

然后把"\C-h": backward-delete-char换成"\C-h": backward-char,然后重新连接即可。

后台异步命令相关快捷键

  • ctrl + c: 终止当前命令
  • ctrl + z: 挂起当前命令,后续可以 bg/fg 重新支持此命令,或 kill 掉
  • ctrl + a: 回到行首
  • ctrl + e: 回到行尾

Logger 命令

提示

查看 logger 信息,更新 logger level

使用参考

查看所有 logger 信息

以下面的logback.xml为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="APPLICATION" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%logger{35} - %msg%n</pattern>
</encoder>
</appender>

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="APPLICATION" />
</appender>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n
</pattern>
<charset>utf8</charset>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC" />
</root>
</configuration>

使用logger命令打印的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[arthas@2062]$ logger
name ROOT
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level INFO
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
target System.out
name APPLICATION
class ch.qos.logback.core.rolling.RollingFileAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
file app.log
name ASYNC
class ch.qos.logback.classic.AsyncAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
appenderRef [APPLICATION]

appenders的信息里,可以看到

  • CONSOLE logger 的 target 是System.out
  • APPLICATION logger 是RollingFileAppender,它的 file 是app.log
  • ASYNC它的appenderRefAPPLICATION,即异步输出到文件里

查看指定名字的 logger 信息

1
2
3
4
5
6
7
8
9
[arthas@2062]$ logger -n org.springframework.web
name org.springframework.web
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar

查看指定 classloader 的 logger 信息

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。

如果你使用-c,你需要手动输入 hashcode:-c <hashcode>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[arthas@2062]$ logger -c 2a139a55
name ROOT
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level DEBUG
effectiveLevel DEBUG
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
target System.out
name APPLICATION
class ch.qos.logback.core.rolling.RollingFileAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
file app.log
name ASYNC
class ch.qos.logback.classic.AsyncAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
appenderRef [APPLICATION]

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

1
logger --classLoaderClass sun.misc.Launcher$AppClassLoader
  • 注: 这里 classLoaderClass 在 java 8 是 sun.misc.Launcher$AppClassLoader,而java 11的classloader是jdk.internal.loader.ClassLoaders$AppClassLoader。

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。

更新 logger level

1
2
[arthas@2062]$ logger --name ROOT --level debug
update logger level success.

指定 classloader 更新 logger level

默认情况下,logger 命令会在 SystemClassloader 下执行,如果应用是传统的war应用,或者 spring boot fat jar 启动的应用,那么需要指定 classloader。

可以先用 sc -d yourClassName 来查看具体的 classloader hashcode,然后在更新 level 时指定 classloader:

1
[arthas@2062]$ logger -c 2a139a55 --name ROOT --level debug

查看没有 appender 的 logger 的信息

默认情况下,logger命令只打印有 appender 的 logger 的信息。如果想查看没有appender的 logger 的信息,可以加上参数--include-no-appender

注意,通常输出结果会很长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[arthas@2062]$ logger --include-no-appender
name ROOT
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level DEBUG
effectiveLevel DEBUG
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
target System.out
name APPLICATION
class ch.qos.logback.core.rolling.RollingFileAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
file app.log
name ASYNC
class ch.qos.logback.classic.AsyncAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
appenderRef [APPLICATION]

name com
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel DEBUG
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar

name com.alibaba
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel DEBUG
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
...

Mbean 命令

提示

查看 Mbean 的信息

这个命令可以便捷的查看或监控 Mbean 的属性信息。

参数说明


mc-redefine 命令

mc

Memory Compiler/内存编译器,编译.java 文件生成.class

使用参考

1
mc /tmp/Test.java

可以通过-c参数指定 classloader:

1
mc -c 327a647b /tmp/Test.java

也可以通过--classLoaderClass参数指定 ClassLoader:

1
2
3
4
$ mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp
Memory compiler output:
/tmp/com/example/demo/arthas/user/UserController.class
Affect(row-cnt:1) cost in 346 ms

可以通过-d命令指定输出目录:

1
mc -d /tmp/output /tmp/ClassA.java /tmp/ClassB.java

编译生成.class文件之后,可以结合retransform命令实现热更新代码。

warn no-icon

注意

注意,mc 命令有可能失败。如果编译失败可以在本地编译好 .class 文件,再上传到服务器。具体参考retransform命令说明。

redefine

提示

推荐使用 retransform 命令

  • redefine 的 class 不能修改、添加、删除类的 field 和 method,包括方法参数、方法名称及返回值
  • 如果 mc 失败,可以在本地开发环境编译好 class 文件,上传到目标系统,使用 redefine 热加载 class
  • 目前 redefine 和 watch/trace/jad/tt 等命令冲突,以后重新实现 redefine 功能会解决此问题

warn no-icon

注意

注意, redefine 后的原来的类不能恢复,redefine 有可能失败(比如增加了新的 field),参考 jdk 本身的文档。

提示

reset命令对redefine的类无效。如果想重置,需要redefine原始的字节码。

提示

redefine命令和jad/watch/trace/monitor/tt等命令会冲突。执行完redefine之后,如果再执行上面提到的命令,则会把redefine的字节码重置。 原因是 jdk 本身 redefine 和 Retransform 是不同的机制,同时使用两种机制来更新字节码,只有最后修改的会生效。


retransform 命令

提示

加载外部的 .class 文件,retransform jvm 已加载的类。

使用参考

1
2
3
4
5
6
7
retransform /tmp/Test.class
retransform -l
retransform -d 1 # delete retransform entry
retransform --deleteAll # delete all retransform entries
retransform --classPattern demo.* # triger retransform classes
retransform -c 327a647b /tmp/Test.class /tmp/Test\$Inner.class
retransform --classLoaderClass 'sun.misc.Launcher$AppClassLoader' /tmp/Test.class

retransform 指定的 .class 文件

1
2
3
$ retransform /tmp/MathGame.class
retransform success, size: 1, classes:
demo.MathGame

加载指定的 .class 文件,然后解析出 class name,再 retransform jvm 中已加载的对应的类。每加载一个 .class 文件,则会记录一个 retransform entry.

提示

如果多次执行 retransform 加载同一个 class 文件,则会有多条 retransform entry.

查看 retransform entry

1
2
3
$ retransform -l
Id ClassName TransformCount LoaderHash LoaderClassName
1 demo.MathGame 1 null null
  • TransformCount 统计在 ClassFileTransformer#transform 函数里尝试返回 entry 对应的 .class 文件的次数,但并不表明 transform 一定成功。

删除指定 retransform entry

需要指定 id:

1
retransform -d 1

删除所有 retransform entry

1
retransform --deleteAll

显式触发 retransform

1
2
3
$ retransform --classPattern demo.MathGame
retransform success, size: 1, classes:
demo.MathGame

注意:对于同一个类,当存在多个 retransform entry 时,如果显式触发 retransform ,则最后添加的 entry 生效(id 最大的)。

消除 retransform 的影响

如果对某个类执行 retransform 之后,想消除影响,则需要:

  • 删除这个类对应的 retransform entry
  • 重新触发 retransform

提示

如果不清除掉所有的 retransform entry,并重新触发 retransform ,则 arthas stop 时,retransform 过的类仍然生效。

结合 jad/mc 命令使用

1
2
3
4
5
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java

mc /tmp/UserController.java -d /tmp

retransform /tmp/com/example/demo/arthas/user/UserController.class
  • jad 命令反编译,然后可以用其它编译器,比如 vim 来修改源码
  • mc 命令来内存编译修改过的代码
  • 用 retransform 命令加载新的字节码

上传 .class 文件到服务器的技巧

使用mc命令来编译jad的反编译的代码有可能失败。可以在本地修改代码,编译好后再上传到服务器上。有的服务器不允许直接上传文件,可以使用base64命令来绕过。

  1. 在本地先转换.class文件为 base64,再保存为 result.txt

    1
    base64 < Test.class > result.txt
  2. 到服务器上,新建并编辑result.txt,复制本地的内容,粘贴再保存

  3. 把服务器上的 result.txt还原为.class

    1
    base64 -d < result.txt > Test.class
  4. 用 md5 命令计算哈希值,校验是否一致

retransform 的限制

  • 不允许新增加 field/method
  • 正在跑的函数,没有退出不能生效,比如下面新增加的System.out.println,只有run()函数里的会生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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());
}
}
}

Monitor 命令

提示

方法执行监控

对匹配 class-patternmethod-patterncondition-express的类、方法的调用进行监控。

monitor 命令是一个非实时返回命令.

实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标 Java 进程返回信息,直到用户输入 Ctrl+C 为止。

服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而不会被执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何 Arthas 命令不会引起原有业务逻辑的改变。

监控的维度说明

监控项 说明
timestamp 时间戳
class Java 类
method 方法(构造方法、普通方法)
total 调用次数
success 成功次数
fail 失败次数
rt 平均 RT
fail-rate 失败率

参数说明

方法拥有一个命名参数 [c:],意思是统计周期(cycle of output),拥有一个整型的参数值

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[E] 开启正则表达式匹配,默认为通配符匹配
[c:] 统计周期,默认值为 120 秒
[b] 方法调用之前计算 condition-express
[m <arg>] 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>]

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ monitor -c 5 demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 94 ms.
timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:38 demo.MathGame primeFactors 5 1 4 1.15 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:43 demo.MathGame primeFactors 5 3 2 42.29 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:48 demo.MathGame primeFactors 5 3 2 67.92 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:53 demo.MathGame primeFactors 5 2 3 0.25 60.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:58 demo.MathGame primeFactors 1 1 0 0.45 0.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:07:03 demo.MathGame primeFactors 2 2 0 3182.72 0.00%

指定 Class 最大匹配数量

1
2
3
4
5
6
7
8
9
10
$ monitor -c 1 -m 1 demo.MathGame primeFactors
Press Q or Ctrl+C to abort.
Affect(class count:1 , method count:1) cost in 384 ms, listenerId: 6.
timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2022-12-25 21:12:58 demo.MathGame primeFactors 1 1 0 0.18 0.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2022-12-25 21:12:59 demo.MathGame primeFactors 0 0 0 0.00 0.00%

计算条件表达式过滤统计结果(方法执行完毕之后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
monitor -c 5 demo.MathGame primeFactors "params[0] <= 2"
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 19 ms, listenerId: 5
timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2020-09-02 09:42:36 demo.MathGame primeFactors 5 3 2 0.09 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:41 demo.MathGame primeFactors 5 2 3 0.11 60.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:46 demo.MathGame primeFactors 5 1 4 0.06 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:51 demo.MathGame primeFactors 5 1 4 0.12 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:56 demo.MathGame primeFactors 5 3 2 0.15 40.00%

#计算条件表达式过滤统计结果(方法执行完毕之前)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
monitor -b -c 5 com.test.testes.MathGame primeFactors "params[0] <= 2"
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 21 ms, listenerId: 4
timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:41:57 demo.MathGame primeFactors 1 0 1 0.10 100.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:02 demo.MathGame primeFactors 3 0 3 0.06 100.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:07 demo.MathGame primeFactors 2 0 2 0.06 100.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:12 demo.MathGame primeFactors 1 0 1 0.05 100.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:17 demo.MathGame primeFactors 2 0 2 0.10 100.00%

ognl 命令

在 Arthas 里,有一个单独的 ognl 命令,可以动态执行代码。

查看用法:ognl --help

参数说明

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

使用参考

调用静态函数:

1
2
$ ognl '@java.lang.System@out.println("hello")'
null

获取静态类的静态字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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],
]

通过 hashcode 指定 ClassLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ classloader -t
+-BootstrapClassLoader
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@301ec38b
+-com.taobao.arthas.agent.ArthasClassloader@472067c7
+-jdk.internal.loader.ClassLoaders$AppClassLoader@4b85612c
+-org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8

$ ognl -c 7f9a81e8 @org.springframework.boot.SpringApplication@logger
@Slf4jLocationAwareLog[
FQCN=@String[org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog],
name=@String[org.springframework.boot.SpringApplication],
logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
]
$

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。

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

1
2
3
4
5
6
$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader  @org.springframework.boot.SpringApplication@logger
@Slf4jLocationAwareLog[
FQCN=@String[org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog],
name=@String[org.springframework.boot.SpringApplication],
logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
]

执行多行表达式,赋值给临时变量,返回一个 List:

1
2
3
4
5
$ 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],
]

Options 命令

全局开关

名称 默认值 描述
unsafe false 是否支持对系统级别的类进行增强,打开该开关可能导致把 JVM 搞挂,请慎重选择!
dump false 是否支持被增强了的类 dump 到外部文件中,如果打开开关,class 文件会被 dump 到/${application working dir}/arthas-class-dump/ 目录下,具体位置详见控制台输出
batch-re-transform true 是否支持批量对匹配到的类执行 retransform 操作
json-format false 是否支持 json 化的输出
disable-sub-class false 是否禁用子类匹配,默认在匹配目标类的时候会默认匹配到其子类,如果想精确匹配,可以关闭此开关
support-default-method true 是否支持匹配到 default method,默认会查找 interface,匹配里面的 default method。参考 #1105
save-result false 是否打开执行结果存日志功能,打开之后所有命令的运行结果都将保存到~/logs/arthas-cache/result.log
job-timeout 1d 异步后台任务的默认超时时间,超过这个时间,任务自动停止;比如设置 1d, 2h, 3m, 25s,分别代表天、小时、分、秒
print-parent-fields true 是否打印在 parent class 里的 filed

查看所有的 options

1
options

获取 option 的值

1
options json-format

默认情况下json-format 为 false,如果希望watch /tt 等命令结果以 json 格式输出,则可以设置json-format 为 true。

设置指定的 option

例如,想打开执行结果存日志功能首先查看日志,发现无记录:

1
cat /root/logs/arthas-cache/result.log

输入如下命令即可激活记录日志功能:

1
options save-result true

稍候片刻,再次查看,发现出现记录:

1
cat /root/logs/arthas-cache/result.log

Perfcounter 命令

查看当前 JVM 的 Perf Counter 信息

使用参考

1
2
3
4
5
6
7
8
9
10
$ perfcounter
java.ci.totalTime 2325637411
java.cls.loadedClasses 3403
java.cls.sharedLoadedClasses 0
java.cls.sharedUnloadedClasses 0
java.cls.unloadedClasses 0
java.property.java.version 11.0.4
java.property.java.vm.info mixed mode
java.property.java.vm.name OpenJDK 64-Bit Server VM
...

可以用-d参数打印更多信息:

1
2
3
4
5
6
7
8
$ perfcounter -d
Name Variability Units Value
---------------------------------------------------------------------------------
java.ci.totalTime Monotonic Ticks 3242526906
java.cls.loadedClasses Monotonic Events 3404
java.cls.sharedLoadedClasses Monotonic Events 0
java.cls.sharedUnloadedClasses Monotonic Events 0
java.cls.unloadedClasses Monotonic Events 0

jdk9 以上的应用

如果没有打印出信息,应用在启动时,加下面的参数:

1
--add-opens java.base/jdk.internal.perf=ALL-UNNAMED --add-exports java.base/jdk.internal.perf=ALL-UNNAMED --add-opens java.management/sun.management.counter.perf=ALL-UNNAMED --add-opens java.management/sun.management.counter=ALL-UNNAMED

Plaintext 命令

将输出结果去除 ANSI 颜色

示例:

1
2
jad demo.MathGame main
jad demo.MathGame main | plaintext

Profiler 命令

使用async-profiler生成火焰图

profiler 命令支持生成应用热点的火焰图。本质上是通过不断的采样,然后把收集到的采样结果生成火焰图。

profiler 命令基本运行结构是 profiler action [actionArg]

参数说明

参数名称 参数说明
action 要执行的操作
actionArg 属性名模式
[i:] 采样间隔(单位:ns)(默认值:10’000’000,即 10 ms)
[f:] 将输出转储到指定路径
[d:] 运行评测指定秒
[e:] 要跟踪哪个事件(cpu, alloc, lock, cache-misses 等),默认是 cpu

启动 profiler

1
2
$ profiler start
Started [cpu] profiling

提示

默认情况下,生成的是 cpu 的火焰图,即 event 为cpu。可以用--event参数来指定。

获取已采集的 sample 的数量

1
2
$ profiler getSamples
23

查看 profiler 状态

1
2
$ profiler status
[cpu] profiling is running for 4 seconds

可以查看当前 profiler 在采样哪种event和采样时间。

停止 profiler

生成 html 格式结果

默认情况下,结果文件是html格式,也可以用--format参数指定:

1
2
3
$ profiler stop --format html
profiler output file: /tmp/test/arthas-output/20211207-111550.html
OK

或者在--file参数里用文件名指名格式。比如--file /tmp/result.html

通过浏览器查看 arthas-output 下面的 profiler 结果

默认情况下,arthas 使用 3658 端口,则可以打开: http://localhost:3658/arthas-output/在新窗口打开 查看到arthas-output目录下面的 profiler 结果:

img

点击可以查看具体的结果:

img

提示

如果是 chrome 浏览器,可能需要多次刷新。

profiler 支持的 events

在不同的平台,不同的 OS 下面,支持的 events 各有不同。比如在 macos 下面:

1
2
3
4
5
6
7
$ profiler list
Basic events:
cpu
alloc
lock
wall
itimer

在 linux 下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ profiler list
Basic events:
cpu
alloc
lock
wall
itimer
Perf events:
page-faults
context-switches
cycles
instructions
cache-references
cache-misses
branches
branch-misses
bus-cycles
L1-dcache-load-misses
LLC-load-misses
dTLB-load-misses
mem:breakpoint
trace:tracepoint

如果遇到 OS 本身的权限/配置问题,然后 缺少部分 event,可以参考async-profiler本身文档:async-profiler在新窗口打开

可以用--event参数指定要采样的事件,比如对alloc事件进入采样:

1
$ profiler start --event alloc

恢复采样

1
2
$ profiler resume
Started [cpu] profiling

startresume的区别是:start是新开始采样,resume会保留上次stop时的数据。

通过执行profiler getSamples可以查看 samples 的数量来验证。

使用execute来执行复杂的命令

比如开始采样:

1
profiler execute 'start,framebuf=5000000'

停止采样,并保存到指定文件里:

1
profiler execute 'stop,file=/tmp/result.html'

具体的格式参考: arguments.cpp在新窗口打开

查看所有支持的 action

1
2
$ profiler actions
Supported Actions: [resume, dumpCollapsed, getSamples, start, list, execute, version, stop, load, dumpFlat, actions, dumpTraces, status]

查看版本

1
2
3
$ profiler version
Async-profiler 1.6 built on Sep 9 2019
Copyright 2019 Andrei Pangin

配置 framebuf 参数

如果遇到生成的火焰图有 [frame_buffer_overflow],则需要增大 framebuf(默认值是 1’000’000),可以显式配置,比如:

1
profiler start --framebuf 5000000

配置 include/exclude 来过滤数据

如果应用比较复杂,生成的内容很多,想只关注部分数据,可以通过 include/exclude 来过滤。比如

1
profiler start --include 'java/*' --include 'demo/*' --exclude '*Unsafe.park*'

include/exclude 都支持设置多个值 ,但是需要配置在命令行的最后。

指定执行时间

比如,希望 profiler 执行 300 秒自动结束,可以用 -d/--duration 参数指定:

1
profiler start --duration 300

生成 jfr 格式结果

注意,jfr 只支持在 start时配置。如果是在stop时指定,则不会生效。

1
profiler start --file /tmp/test.jfr

file参数支持一些变量:

  • 时间戳: --file /tmp/test-%t.jfr
  • 进程 ID: --file /tmp/test-%p.jfr

生成的结果可以用支持 jfr 格式的工具来查看。比如:

生成的火焰图里的 unknown


Pwd 命令

通过 pwd 命令可以获知当前的工作目录,和 linux 命令类似

1
pwd

Quit-stop 命令

退出 Arthas

exit 或者 quit 命令可以退出 Arthas。

退出 Arthas 之后,还可以再次用 java -jar arthas-boot.jar 来连接。

彻底退出 Arthas

exit/quit 命令 只是退出当前 session,arthas server 还在目标进程中运行。

想完全退出 Arthas,可以执行 stop 命令。


Reset 命令

提示

重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端stop时会重置所有增强过的类

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ reset -h
USAGE:
reset [-h] [-E] [class-pattern]

SUMMARY:
Reset all the enhanced classes

EXAMPLES:
reset
reset *List
reset -E .*List

OPTIONS:
-h, --help this help
-E, --regex Enable regular expression to match (wildcard matching by default)
<class-pattern> Path and classname of Pattern Matching

还原指定类

1
2
3
4
5
6
7
8
9
10
11
$ trace Test test
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 57 ms.
`---ts=2017-10-26 17:10:33;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@14dad5dc
`---[0.590102ms] Test:test()

`---ts=2017-10-26 17:10:34;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@14dad5dc
`---[0.068692ms] Test:test()

$ reset Test
Affect(class-cnt:1 , method-cnt:0) cost in 11 ms.

还原所有类

1
2
3
4
5
6
7
8
$ trace Test test
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 15 ms.
`---ts=2017-10-26 17:12:06;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@14dad5dc
`---[0.128518ms] Test:test()

$ reset
Affect(class-cnt:1 , method-cnt:0) cost in 9 ms.

Sc 命令

查看 JVM 已加载的类信息

“Search-Class”的简写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息,这个命令支持的参数有 [d][E][f][x:]

参数说明

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

sc 命令文档

class-pattern 支持全限定名,如 com.taobao.test.AAA,也支持 com/taobao/test/AAA 这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把/ 替换为. 啦。

sc 默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true 开关

使用参考

  • 模糊搜索 sc demo.*
  • 打印类的详细信息 sc -d demo.MathGame

指定 classLoader

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。
如果你使用-c ,你需要手动输入 hashcode:-c <hashcode>
对于只有唯一实例的 ClassLoader 可以通过--classLoaderClass 指定 class name,使用起来更加方便:

1
sc --classLoaderClass jdk.internal.loader.ClassLoaders$AppClassLoader -d demo*
  • 注:这里 classLoaderClass 在 java 8 是 sun.misc.Launcher$AppClassLoader,而 java 11 的 classloader 是 jdk.internal.loader.ClassLoaders$AppClassLoader,killercoda 目前环境是 java11。

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode> 是动态变化的。

  • 打印出类的 Field 信息 sc -d -f demo.MathGame

session 命令

查看当前会话的信息,显示当前绑定的 pid 以及会话 id。

提示

如果配置了 tunnel server,会追加打印 代理 id、tunnel 服务器的 url 以及连接状态。

如果使用了 staturl 做统计,会追加显示 statUrl 地址。

使用参考

1
2
3
4
5
$ session
Name Value
--------------------------------------------------
JAVA_PID 14584
SESSION_ID c2073d3b-443a-4a9b-9249-0c5d24a5756c

Sm 命令

提示

查看已加载类的方法信息

“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息。

sm 命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。

参数说明

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

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ sm java.lang.String
java.lang.String-><init>
java.lang.String->equals
java.lang.String->toString
java.lang.String->hashCode
java.lang.String->compareTo
java.lang.String->indexOf
java.lang.String->valueOf
java.lang.String->checkBounds
java.lang.String->length
java.lang.String->isEmpty
java.lang.String->charAt
java.lang.String->codePointAt
java.lang.String->codePointBefore
java.lang.String->codePointCount
java.lang.String->offsetByCodePoints
java.lang.String->getChars
java.lang.String->getBytes
java.lang.String->contentEquals
java.lang.String->nonSyncContentEquals
java.lang.String->equalsIgnoreCase
java.lang.String->compareToIgnoreCase
java.lang.String->regionMatches
java.lang.String->startsWith
java.lang.String->endsWith
java.lang.String->indexOfSupplementary
java.lang.String->lastIndexOf
java.lang.String->lastIndexOfSupplementary
java.lang.String->substring
java.lang.String->subSequence
java.lang.String->concat
java.lang.String->replace
java.lang.String->matches
java.lang.String->contains
java.lang.String->replaceFirst
java.lang.String->replaceAll
java.lang.String->split
java.lang.String->join
java.lang.String->toLowerCase
java.lang.String->toUpperCase
java.lang.String->trim
java.lang.String->toCharArray
java.lang.String->format
java.lang.String->copyValueOf
java.lang.String->intern
Affect(row-cnt:44) cost in 1342 ms.
1
2
3
4
5
6
7
8
9
10
$ sm -d java.lang.String toString
declaring-class java.lang.String
method-name toString
modifier public
annotation
parameters
return java.lang.String
exceptions

Affect(row-cnt:1) cost in 3 ms.

Stack 命令

提示

输出当前方法被调用的调用路径

很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[E] 开启正则表达式匹配,默认为通配符匹配
[n:] 执行次数限制
[m <arg>] 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>]

这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。

观察的维度也比较多,主要体现在参数 advice 的数据结构上。Advice 参数最主要是封装了通知节点的所有信息。

请参考表达式核心变量中关于该节点的描述。

使用例子

stack

1
2
3
4
5
6
$ stack demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 36 ms.
ts=2018-12-04 01:32:19;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
@demo.MathGame.run()
at demo.MathGame.main(MathGame.java:16)

指定 Class 最大匹配数量

1
2
3
4
5
6
7
$ stack demo.MathGame primeFactors -m 1
Press Q or Ctrl+C to abort.
Affect(class count:1 , method count:1) cost in 561 ms, listenerId: 5.
ts=2022-12-25 21:07:07;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@b4aac2
@demo.MathGame.primeFactors()
at demo.MathGame.run(MathGame.java:46)
at demo.MathGame.main(MathGame.java:38)

据条件表达式来过滤

1
2
3
4
5
6
7
8
9
10
11
12
$ stack demo.MathGame primeFactors 'params[0]<0' -n 2
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 30 ms.
ts=2018-12-04 01:34:27;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
@demo.MathGame.run()
at demo.MathGame.main(MathGame.java:16)

ts=2018-12-04 01:34:30;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
@demo.MathGame.run()
at demo.MathGame.main(MathGame.java:16)

Command execution times exceed limit: 2, so command will exit. You can set it with -n option.

据执行时间来过滤

1
2
3
4
5
6
$ stack demo.MathGame primeFactors '#cost>5'
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 35 ms.
ts=2018-12-04 01:35:58;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
@demo.MathGame.run()
at demo.MathGame.main(MathGame.java:16)

Sysenv 命令

提示

查看当前 JVM 的环境属性( System Environment Variables )

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
USAGE:
sysenv [-h] [env-name]

SUMMARY:
Display the system env.

EXAMPLES:
sysenv
sysenv USER

WIKI:
https://arthas.aliyun.com/doc/sysenv

OPTIONS:
-h, --help this help
<env-name> env name

查看所有环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ sysenv
KEY VALUE
----------------------------------------------------------------------------------------------------------------------------
PATH /Users/admin/.sdkman/candidates/visualvm/current/bin:/Users/admin/.sdkman/candidates/ja
va/current/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/Wireshark.app/Contents/
MacOS
SDKMAN_VERSION 5.7.3+337
JAVA_HOME /Users/admin/.sdkman/candidates/java/current
JAVA_MAIN_CLASS_65244 demo.MathGame
TERM xterm-256color
LANG zh_CN.UTF-8
AUTOJUMP_SOURCED 1
COLORTERM truecolor
LOGNAME admin
XPC_SERVICE_NAME 0
PWD /Users/admin/code/ali/arthas/demo
TERM_PROGRAM_VERSION 3.2.5
_ /Users/admin/.sdkman/candidates/java/current/bin/java
SHELL /bin/bash
TERM_PROGRAM iTerm.app
SDKMAN_PLATFORM Darwin
USER admin
ITERM_PROFILE Default
TMPDIR /var/folders/0r/k561bkk917gg972stqclbz9h0000gn/T/
XPC_FLAGS 0x0
TERM_SESSION_ID w0t4p0:60BC264D-9649-42AC-A7E4-AF85B69F93F8
__CF_USER_TEXT_ENCODING 0x1F5:0x19:0x34
Apple_PubSub_Socket_Ren /private/tmp/com.apple.launchd.DwmmjSQsll/Render
der
COLORFGBG 7;0
HOME /Users/admin
SHLVL 1
AUTOJUMP_ERROR_PATH /Users/admin/Library/autojump/errors.log

查看单个环境变量

提示

支持通过TAB键自动补全

1
2
$ sysenv USER
USER=admin

sysprop 命令

提示

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

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
USAGE:
sysprop [-h] [property-name] [property-value]

SUMMARY:
Display, and change all the system properties.

EXAMPLES:
sysprop
sysprop file.encoding
sysprop production.mode true

WIKI:
https://arthas.aliyun.com/doc/sysprop

OPTIONS:
-h, --help this help
<property-name> property name
<property-value> property value

查看所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
$ sysprop
KEY VALUE
-------------------------------------------------------------------------------------------------------------------------------------
java.runtime.name Java(TM) SE Runtime Environment
sun.boot.library.path /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib
java.vm.version 25.51-b03
user.country.format CN
gopherProxySet false
java.vm.vendor Oracle Corporation
java.vendor.url http://java.oracle.com/
path.separator :
java.vm.name Java HotSpot(TM) 64-Bit Server VM
file.encoding.pkg sun.io
user.country US
sun.java.launcher SUN_STANDARD
sun.os.patch.level unknown
java.vm.specification.name Java Virtual Machine Specification
user.dir /private/var/tmp
java.runtime.version 1.8.0_51-b16
java.awt.graphicsenv sun.awt.CGraphicsEnvironment
java.endorsed.dirs /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/endors
ed
os.arch x86_64
java.io.tmpdir /var/folders/2c/tbxwzs4s4sbcvh7frbcc7n000000gn/T/
line.separator

java.vm.specification.vendor Oracle Corporation
os.name Mac OS X
sun.jnu.encoding UTF-8
java.library.path /Users/wangtao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Libra
ry/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
sun.nio.ch.bugLevel
java.specification.name Java Platform API Specification
java.class.version 52.0
sun.management.compiler HotSpot 64-Bit Tiered Compilers
os.version 10.12.6
user.home /Users/wangtao
user.timezone Asia/Shanghai
java.awt.printerjob sun.lwawt.macosx.CPrinterJob
file.encoding UTF-8
java.specification.version 1.8
user.name wangtao
java.class.path .
java.vm.specification.version 1.8
sun.arch.data.model 64
java.home /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre
sun.java.command Test
java.specification.vendor Oracle Corporation
user.language en
awt.toolkit sun.lwawt.macosx.LWCToolkit
java.vm.info mixed mode
java.version 1.8.0_51
java.ext.dirs /Users/wangtao/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.
8.0_51.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library
/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.boot.class.path /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resour
ces.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/li
b/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/l
ib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/H
ome/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Content
s/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Conte
nts/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jd
k/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.
jdk/Contents/Home/jre/classes
java.vendor Oracle Corporation
file.separator /
java.vendor.url.bug http://bugreport.sun.com/bugreport/
sun.cpu.endian little
sun.io.unicode.encoding UnicodeBig
sun.cpu.isalist

查看单个属性

提示

支持通过TAB键自动补全

1
2
$ sysprop java.version
java.version=1.8.0_51

修改单个属性

1
2
3
4
5
$ sysprop user.country
user.country=US
$ sysprop user.country CN
Successfully changed the system property.
user.country=CN

tee 命令

类似传统的 tee 命令 用于读取标准输入的数据,并将其内容输出成文件。

tee 指令会从标准输入设备读取数据,将其内容输出到标准输出设备,同时保存成文件。

使用 tee -h 查看帮助信息

命令示范

将 sysprop 命令执行结果另外存储在/tmp/logfile

1
sysprop | tee /tmp/logfile

查看/tmp/logfile 文件:

1
cat /tmp/logfile

将 sysprop 命令执行结果匹配java 后另外追加在/tmp/logfile

1
sysprop | grep java | tee -a /path/to/logfile

查看/tmp/logfile 文件:

1
cat /tmp/logfile

thread 命令

查看当前线程信息,查看线程的堆栈

参数说明

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

Thread 用法

支持一键展示当前最忙的前 N 个线程并打印堆栈:

1
thread -n 3
  • 没有线程 ID,包含[Internal] 表示为 JVM 内部线程,参考dashboard 命令的介绍。
  • cpuUsage 为采样间隔时间内线程的 CPU 使用率,与dashboard 命令的数据一致。
  • deltaTime 为采样间隔时间内线程的增量 CPU 时间,小于 1ms 时被取整显示为 0ms。
  • time 线程运行总 CPU 时间。

注意:线程栈为第二采样结束时获取,不能表明采样间隔时间内该线程都是在处理相同的任务。建议间隔时间不要太长,可能间隔时间越大越不准确。 可以根据具体情况尝试指定不同的间隔时间,观察输出结果。

当没有参数时,显示第一页线程信息

默认按照 CPU 增量时间降序排列,只显示第一页数据,避免滚屏。

1
thread

thread –all, 显示所有匹配的线程

显示所有匹配线程信息,有时需要获取全部 JVM 的线程数据进行分析。

1
thread --all

thread id,显示指定线程的运行堆栈

查看线程 ID 16 的栈:

1
thread 16

thread -b, 找出当前阻塞其他线程的线程

有时候我们发现应用卡住了,通常是由于某个线程拿住了某个锁,并且其他线程都在等待这把锁造成的。为了排查这类问题,arthas 提供了thread -b ,一键找出那个罪魁祸首。

1
thread -b

注意,目前只支持找出 synchronized 关键字阻塞住的线程,如果是java.util.concurrent.Lock ,目前还不支持。

thread -i, 指定采样时间间隔

  • thread -i 1000 : 统计最近 1000ms 内的线程 CPU 时间。
1
thread -i 1000
  • thread -n 3 -i 1000 : 列出 1000ms 内最忙的 3 个线程栈
1
thread -n 3 -i 1000

thread –state,查看指定状态的线程

1
thread --state WAITING

trace 命令

提示

方法内部调用路径,并输出方法路径上的每个节点上耗时

trace 命令能主动搜索 class-patternmethod-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[E] 开启正则表达式匹配,默认为通配符匹配
[n:] 命令执行次数
#cost 方法执行耗时
[m <arg>] 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>]

这里重点要说明的是条件表达式条件表达式的构成主要由 ognl 表达式组成,所以你可以这样写"params[0]<0",只要是一个合法的 ognl 表达式,都能被正常支持。

请参考表达式核心变量中关于该节点的描述。

很多时候我们只想看到某个方法的 rt 大于某个时间之后的 trace 结果,现在 Arthas 可以按照方法执行的耗时来进行过滤了,例如trace *StringUtils isBlank '#cost>100'表示当执行时间超过 100ms 的时候,才会输出 trace 的结果。

提示

watch/stack/trace 这个三个命令都支持#cost

注意事项

  • trace 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路。

    参考:Trace 命令的实现原理在新窗口打开

  • 3.3.0 版本后,可以使用动态 Trace 功能,不断增加新的匹配类,参考下面的示例。

  • 目前不支持 trace java.lang.Thread getName,参考 issue: #1610在新窗口打开 ,考虑到不是非常必要场景,且修复有一定难度,因此当前暂不修复

使用参考

trace 函数

1
2
3
4
5
6
7
8
9
10
$ trace demo.MathGame run
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 28 ms.
`---ts=2019-12-04 00:45:08;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[0.617465ms] demo.MathGame:run()
`---[0.078946ms] demo.MathGame:primeFactors() #24 [throws Exception]

`---ts=2019-12-04 00:45:09;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[1.276874ms] demo.MathGame:run()
`---[0.03752ms] demo.MathGame:primeFactors() #24 [throws Exception]

提示

结果里的 #24,表示在 run 函数里,在源文件的第24行调用了primeFactors()函数。

指定 Class 匹配的最大数量

1
2
3
4
5
6
7
8
9
10
$ trace demo.MathGame run -m 1
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 412 ms, listenerId: 4
`---ts=2022-12-25 21:00:00;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@b4aac2
`---[0.762093ms] demo.MathGame:run()
`---[30.21% 0.230241ms] demo.MathGame:primeFactors() #46 [throws Exception]

`---ts=2022-12-25 21:00:10;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@b4aac2
`---[0.315298ms] demo.MathGame:run()
`---[13.95% 0.043995ms] demo.MathGame:primeFactors() #46 [throws Exception]

trace 次数限制

如果方法调用的次数很多,那么可以用-n参数指定捕捉结果的次数。比如下面的例子里,捕捉到一次调用就退出命令。

1
2
3
4
5
6
7
8
9
$ trace demo.MathGame run -n 1
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 20 ms.
`---ts=2019-12-04 00:45:53;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[0.549379ms] demo.MathGame:run()
+---[0.059839ms] demo.MathGame:primeFactors() #24
`---[0.232887ms] demo.MathGame:print() #25

Command execution times exceed limit: 1, so command will exit. You can set it with -n option.

包含 jdk 的函数

  • --skipJDKMethod <value> skip jdk method trace, default value true.

默认情况下,trace 不会包含 jdk 里的函数调用,如果希望 trace jdk 里的函数,需要显式设置--skipJDKMethod false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ trace --skipJDKMethod false demo.MathGame run
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 60 ms.
`---ts=2019-12-04 00:44:41;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[1.357742ms] demo.MathGame:run()
+---[0.028624ms] java.util.Random:nextInt() #23
+---[0.045534ms] demo.MathGame:primeFactors() #24 [throws Exception]
+---[0.005372ms] java.lang.StringBuilder:<init>() #28
+---[0.012257ms] java.lang.Integer:valueOf() #28
+---[0.234537ms] java.lang.String:format() #28
+---[min=0.004539ms,max=0.005778ms,total=0.010317ms,count=2] java.lang.StringBuilder:append() #28
+---[0.013777ms] java.lang.Exception:getMessage() #28
+---[0.004935ms] java.lang.StringBuilder:toString() #28
`---[0.06941ms] java.io.PrintStream:println() #28

`---ts=2019-12-04 00:44:42;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[3.030432ms] demo.MathGame:run()
+---[0.010473ms] java.util.Random:nextInt() #23
+---[0.023715ms] demo.MathGame:primeFactors() #24 [throws Exception]
+---[0.005198ms] java.lang.StringBuilder:<init>() #28
+---[0.006405ms] java.lang.Integer:valueOf() #28
+---[0.178583ms] java.lang.String:format() #28
+---[min=0.011636ms,max=0.838077ms,total=0.849713ms,count=2] java.lang.StringBuilder:append() #28
+---[0.008747ms] java.lang.Exception:getMessage() #28
+---[0.019768ms] java.lang.StringBuilder:toString() #28
`---[0.076457ms] java.io.PrintStream:println() #28

根据调用耗时过滤

1
2
3
4
5
6
7
8
$ trace demo.MathGame run '#cost > 10'
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 41 ms.
`---ts=2018-12-04 01:12:02;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[12.033735ms] demo.MathGame:run()
+---[0.006783ms] java.util.Random:nextInt()
+---[11.852594ms] demo.MathGame:primeFactors()
`---[0.05447ms] demo.MathGame:print()

提示

只会展示耗时大于 10ms 的调用路径,有助于在排查问题的时候,只关注异常情况

  • 是不是很眼熟,没错,在 JProfiler 等收费软件中你曾经见识类似的功能,这里你将可以通过命令就能打印出指定调用路径。 友情提醒下,trace 在执行的过程中本身是会有一定的性能开销,在统计的报告中并未像 JProfiler 一样预先减去其自身的统计开销。所以这统计出来有些许的不准,渲染路径上调用的类、方法越多,性能偏差越大。但还是能让你看清一些事情的。
  • [12.033735ms] 的含义,12.033735 的含义是:当前节点在当前步骤的耗时,单位为毫秒
  • [0,0,0ms,11]xxx:yyy() [throws Exception],对该方法中相同的方法调用进行了合并,0,0,0ms,11 表示方法调用耗时,min,max,total,countthrows Exception 表明该方法调用中存在异常返回
  • 这里存在一个统计不准确的问题,就是所有方法耗时加起来可能会小于该监测方法的总耗时,这个是由于 Arthas 本身的逻辑会有一定的耗时

trace 多个类或者多个函数

trace 命令只会 trace 匹配到的函数里的子调用,并不会向下 trace 多层。因为 trace 是代价比较贵的,多层 trace 可能会导致最终要 trace 的类和函数非常多。

可以用正则表匹配路径上的多个类和函数,一定程度上达到多层 trace 的效果。

1
trace -E com.test.ClassA|org.test.ClassB method1|method2|method3

排除掉指定的类

使用 --exclude-class-pattern 参数可以排除掉指定的类,比如:

1
trace javax.servlet.Filter * --exclude-class-pattern com.demo.TestFilter

动态 trace

提示

3.3.0 版本后支持。

打开终端 1,trace 上面 demo 里的run函数,可以看到打印出 listenerId: 1

1
2
3
4
5
6
7
8
9
10
11
[arthas@59161]$ trace demo.MathGame run
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 112 ms, listenerId: 1
`---ts=2020-07-09 16:48:11;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[1.389634ms] demo.MathGame:run()
`---[0.123934ms] demo.MathGame:primeFactors() #24 [throws Exception]

`---ts=2020-07-09 16:48:12;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[3.716391ms] demo.MathGame:run()
+---[3.182813ms] demo.MathGame:primeFactors() #24
`---[0.167786ms] demo.MathGame:print() #25

现在想要深入子函数primeFactors,可以打开一个新终端 2,使用telnet localhost 3658连接上 arthas,再 trace primeFactors时,指定listenerId

1
2
3
[arthas@59161]$ trace demo.MathGame primeFactors --listenerId 1
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 34 ms, listenerId: 1

这时终端 2 打印的结果,说明已经增强了一个函数:Affect(class count: 1 , method count: 1),但不再打印更多的结果。

再查看终端 1,可以发现 trace 的结果增加了一层,打印了primeFactors函数里的内容:

1
2
3
4
5
6
7
8
9
10
11
`---ts=2020-07-09 16:49:29;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[0.492551ms] demo.MathGame:run()
`---[0.113929ms] demo.MathGame:primeFactors() #24 [throws Exception]
`---[0.061462ms] demo.MathGame:primeFactors()
`---[0.001018ms] throw:java.lang.IllegalArgumentException() #46

`---ts=2020-07-09 16:49:30;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[0.409446ms] demo.MathGame:run()
+---[0.232606ms] demo.MathGame:primeFactors() #24
| `---[0.1294ms] demo.MathGame:primeFactors()
`---[0.084025ms] demo.MathGame:print() #25

通过指定listenerId的方式动态 trace,可以不断深入。另外 watch/tt/monitor等命令也支持类似的功能。

trace 结果时间不准确问题

比如下面的结果里:0.705196 > (0.152743 + 0.145825)

1
2
3
4
5
6
7
$ 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参数可以打印出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $ 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,进入同步块等。

使用 -v 参数打印更多信息

提示

watch/trace/monitor/stack/tt 命令都支持 -v 参数

当命令执行之后,没有输出结果。有两种可能:

  1. 匹配到的函数没有被执行
  2. 条件表达式结果是 false

但用户区分不出是哪种情况。

使用 -v选项,则会打印Condition express的具体值和执行结果,方便确认。


tt 命令

提示

方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

watch 虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。

这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。

于是乎,TimeTunnel 命令就诞生了。

注意事项

  • tt 命令的实现是:把函数的入参/返回值等,保存到一个Map<Integer, TimeFragment>里,默认的大小是 100。
  • tt 相关功能在使用完之后,需要手动释放内存,否则长时间可能导致OOM。退出 arthas 不会自动清除 tt 的缓存 map。

使用参考

记录调用

对于一个最基本的使用来说,就是记录下当前方法的每次调用环境现场。

1
2
3
4
5
6
7
8
9
10
$ tt -t demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 66 ms.
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors

指定 Class 最大匹配数量

1
2
3
4
5
6
7
$ tt -t -m 1 demo.MathGame primeFactors
Press Q or Ctrl+C to abort.
Affect(class count:1 , method count:1) cost in 130 ms, listenerId: 1.
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2022-12-25 19:41:45 2.629929 true false 0x3bf400 MathGame primeFactors
1001 2022-12-25 19:41:55 0.146161 false true 0x3bf400 MathGame primeFactors
  • 命令参数解析

    • -t

      tt 命令有很多个主参数,-t 就是其中之一。这个参数的表明希望记录下类 *Testprint 方法的每次执行情况。

    • -n 3

      当你执行一个调用量不高的方法时可能你还能有足够的时间用 CTRL+C 中断 tt 命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的 JVM 内存撑爆。

      此时你可以通过 -n 参数指定你需要记录的次数,当达到记录次数时 Arthas 会主动中断 tt 命令的记录过程,避免人工操作无法停止的情况。

    • -m 1

      通过 -m 参数指定 Class 匹配的最大数量,防止匹配到的 Class 数量太多导致 JVM 挂起,默认值是 50。

  • 表格字段说明

表格字段 字段解释
INDEX 时间片段记录编号,每一个编号代表着一次调用,后续 tt 还有很多命令都是基于此编号指定记录操作,非常重要。
TIMESTAMP 方法执行的本机时间,记录了这个时间片段所发生的本机时间
COST(ms) 方法执行的耗时
IS-RET 方法是否以正常返回的形式结束
IS-EXP 方法是否以抛异常的形式结束
OBJECT 执行对象的hashCode(),注意,曾经有人误认为是对象在 JVM 中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体
CLASS 执行的类名
METHOD 执行的方法名
  • 条件表达式

    不知道大家是否有在使用过程中遇到以下困惑

    • Arthas 似乎很难区分出重载的方法
    • 我只需要观察特定参数,但是 tt 却全部都给我记录了下来

    条件表达式也是用 OGNL 来编写,核心的判断对象依然是 Advice 对象。除了 tt 命令之外,watchtracestack 命令也都支持条件表达式。

  • 解决方法重载

    tt -t *Test print params.length==1

    通过制定参数个数的形式解决不同的方法签名,如果参数个数一样,你还可以这样写

    tt -t *Test print 'params[1] instanceof Integer'

  • 解决指定参数

    tt -t *Test print params[0].mobile=="13989838402"

  • 构成条件表达式的 Advice 对象

    前边看到了很多条件表达式中,都使用了 params[0],有关这个变量的介绍,请参考表达式核心变量

检索调用记录

当你用 tt 记录了一大片的时间片段之后,你希望能从中筛选出自己需要的时间片段,这个时候你就需要对现有记录进行检索。

假设我们有这些记录

1
2
3
4
5
6
7
8
9
10
11
$ tt -l
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 4 ms.

我需要筛选出 primeFactors 方法的调用信息

1
2
3
4
5
6
7
8
9
10
11
$ tt -s 'method.name=="primeFactors"'
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 607 ms.

你需要一个 -s 参数。同样的,搜索表达式的核心对象依旧是 Advice 对象。

查看调用信息

对于具体一个时间片的信息而言,你可以通过 -i 参数后边跟着对应的 INDEX 编号查看到他的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tt -i 1003
INDEX 1003
GMT-CREATE 2018-12-04 11:15:41
COST(ms) 0.186073
OBJECT 0x4b67cf4d
CLASS demo.MathGame
METHOD primeFactors
IS-RETURN false
IS-EXCEPTION true
PARAMETERS[0] @Integer[-564322413]
THROW-EXCEPTION java.lang.IllegalArgumentException: number is: -564322413, need >= 2
at demo.MathGame.primeFactors(MathGame.java:46)
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(MathGame.java:16)

Affect(row-cnt:1) cost in 11 ms.

重做一次调用

当你稍稍做了一些调整之后,你可能需要前端系统重新触发一次你的调用,此时得求爷爷告奶奶的需要前端配合联调的同学再次发起一次调用。而有些场景下,这个调用不是这么好触发的。

tt 命令由于保存了当时调用的所有现场信息,所以我们可以自己主动对一个 INDEX 编号的时间片自主发起一次调用,从而解放你的沟通成本。此时你需要 -p 参数。通过 --replay-times 指定 调用次数,通过 --replay-interval 指定多次调用间隔(单位 ms, 默认 1000ms)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ tt -i 1004 -p
RE-INDEX 1004
GMT-REPLAY 2018-12-04 11:26:00
OBJECT 0x4b67cf4d
CLASS demo.MathGame
METHOD primeFactors
PARAMETERS[0] @Integer[946738738]
IS-RETURN true
IS-EXCEPTION false
COST(ms) 0.186073
RETURN-OBJ @ArrayList[
@Integer[2],
@Integer[11],
@Integer[17],
@Integer[2531387],
]
Time fragment[1004] successfully replayed.
Affect(row-cnt:1) cost in 14 ms.

你会发现结果虽然一样,但调用的路径发生了变化,由原来的程序发起变成了 Arthas 自己的内部线程发起的调用了。

观察表达式

-w, --watch-express 观察时空隧道使用ognl 表达式

1
2
3
4
5
6
7
8
9
[arthas@10718]$ tt -t demo.MathGame run -n 5
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 56 ms, listenerId: 1
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1000 2021-01-08 21:54:17 0.901091 true false 0x7699a589 MathGame run
[arthas@10718]$ tt -w 'target.illegalArgumentCount' -x 1 -i 1000
@Integer[60]
Affect(row-cnt:1) cost in 7 ms.
  • 获取类的静态字段、调用类的静态方法
1
2
3
4
5
6
7
8
[arthas@10718]$ tt -t demo.MathGame run -n 5
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 56 ms, listenerId: 1
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1000 2021-01-08 21:54:17 0.901091 true false 0x7699a589 MathGame run
[arthas@10718]$ tt -w '@demo.MathGame@random.nextInt(100)' -x 1 -i 1000
@Integer[46]

注意这里使用 com.taobao.arthas.core.advisor.Advice#getLoader加载,使用精确classloader ognl更好。

高级用法 获取 spring context 调用 bean 方法在新窗口打开

  • 需要强调的点

    1. ThreadLocal 信息丢失

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

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

    2. 引用的对象

      需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。

通过索引删除指定的 tt 记录

1
tt -d 1001

清除所有的 tt 记录

1
tt --delete-all

version 命令

输出当前目标 Java 进程所加载的 Arthas 版本号

使用参考

1
2
$ version
3.5.1

Vmoption 命令

使用参考

查看所有的 option

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[arthas@56963]$ vmoption
KEY VALUE ORIGIN WRITEABLE
---------------------------------------------------------------------------------------------
HeapDumpBeforeFullGC false DEFAULT true
HeapDumpAfterFullGC false DEFAULT true
HeapDumpOnOutOfMemory false DEFAULT true
Error
HeapDumpPath DEFAULT true
CMSAbortablePrecleanW 100 DEFAULT true
aitMillis
CMSWaitDuration 2000 DEFAULT true
CMSTriggerInterval -1 DEFAULT true
PrintGC false DEFAULT true
PrintGCDetails true MANAGEMENT true
PrintGCDateStamps false DEFAULT true
PrintGCTimeStamps false DEFAULT true
PrintGCID false DEFAULT true
PrintClassHistogramBe false DEFAULT true
foreFullGC
PrintClassHistogramAf false DEFAULT true
terFullGC
PrintClassHistogram false DEFAULT true
MinHeapFreeRatio 0 DEFAULT true
MaxHeapFreeRatio 100 DEFAULT true
PrintConcurrentLocks false DEFAULT true

查看指定的 option

1
2
3
4
$ vmoption PrintGC
KEY VALUE ORIGIN WRITEABLE
---------------------------------------------------------------------------------
PrintGC false MANAGEMENT true

更新指定的 option

1
2
3
4
5
$ vmoption PrintGC true
Successfully updated the vm option.
NAME BEFORE-VALUE AFTER-VALUE
------------------------------------
PrintGC false true

再使用vmtool 命令执行强制 GC,则可以在Tab 1 看到打印出 GC 日志:

1
vmtool --action forceGc

配置打印 GC 详情

1
vmoption PrintGCDetails true

再使用vmtool 命令执行强制 GC,则可以在Tab 1 看到打印出 GC 详情:

1
vmtool --action forceGc

vmtool 命令

提示

@since 3.5.1

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

查找 jvm 里的字符串对象

1
vmtool --action getInstances --className java.lang.String

limit 参数

通过 --limit 参数,可以限制返回值数量,避免获取超大数据时对 JVM 造成压力。默认值是 10。

所以上面的命令实际上等值于:

1
vmtool --action getInstances --className java.lang.String --limit 10

如果设置--limit 为负数,则遍历所有对象。

下面使用vmtool 命令查找 spring 里的对象。

查找 spring context

1
vmtool --action getInstances --className org.springframework.context.ApplicationContext

指定返回结果展开层数

getInstances action 返回结果绑定到instances 变量上,它是数组。

通过 -x /--expand 参数可以指定结果的展开层次,默认值是 1。

1
vmtool --action getInstances --className org.springframework.context.ApplicationContext -x 2

执行表达式

getInstances action 返回结果绑定到instances 变量上,它是数组。可以通过--express 参数执行指定的表达式。

查找所有的 spring beans 名字:

1
vmtool --action getInstances --className org.springframework.context.ApplicationContext --express 'instances[0].getBeanDefinitionNames()'

调用userController.findUserById(1) 函数:

1
vmtool --action getInstances --className org.springframework.context.ApplicationContext --express 'instances[0].getBean("userController").findUserById(1)'

直接获取 UserController 并调用方法

1
vmtool --action getInstances --className com.example.demo.arthas.user.UserController --express 'instances[0].findUserById(1)'

指定 classloader

可以通过sc 命令查找到加载 class 的 classloader。

1
sc -d org.springframework.context.ApplicationContext

通过上面命令得到 org.springframework.boot.loader.LaunchedURLClassLoader hashcode 之后用-c / --classloader 参数指定,这里使用 --classLoaderClass 来指定 classloader

1
vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext

强制 GC

1
vmtool --action forceGc

interrupt 指定线程

thread id 通过-t参数指定,可以使用 thread命令获取。

1
vmtool --action interruptThread -t 1

watch 命令

方法执行数据观测

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

参数说明

watch 的参数比较多,主要是因为它能在 4 个不同的场景观察对象

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

这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params,returnObj}" ,只要是一个合法的 ognl 表达式,都能被正常支持。

观察的维度也比较多,主要体现在参数 advice 的数据结构上。Advice 参数最主要是封装了通知节点的所有信息。请参考表达式核心变量中关于该节点的描述。

特别说明

  • watch 命令定义了 4 个观察事件点,即 -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后
  • 4 个观察事件点 -b-e-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
  • 这里要注意方法入参方法出参 的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表方法入参外,其余事件都代表方法出参
  • 当使用 -b 时,由于观察事件点是在方法调用前,此时返回值或异常均不存在

使用参考

观察方法出参、this 对象和返回值

观察表达式,默认值是{params, target, returnObj}

1
watch demo.MathGame primeFactors -x 2

Q 或者 Ctrl+c 退出

  • 从运行结果里,说明函数被执行了两次,第一次结果是location=AtExceptionExit ,说明函数抛出异常了,因此returnObj 是 null
  • 在第二次结果里是location=AtExit ,说明函数正常返回,因此可以看到returnObj 结果是一个 ArrayList

观察方法入参

1
watch demo.MathGame primeFactors "{params,returnObj}" -x 2 -b

Q 或者 Ctrl+c 退出

  • 对比前一个例子,返回值为空(事件点为方法执行前,因此获取不到返回值)

同时观察方法调用前和方法返回后

1
watch demo.MathGame primeFactors "{params,target,returnObj}" -x 2 -b -s -n 2
  • 参数里-n 2 ,表示只执行两次
  • 这里输出结果中,第一次输出的是方法调用前的观察表达式的结果,第二次输出的是方法返回后的表达式的结果
  • 结果的输出顺序和事件发生的先后顺序一致,和命令中 -s -b 的顺序无关

调整-x 的值,观察具体的方法参数值

1
watch demo.MathGame primeFactors "{params,target}" -x 3

Q 或者 Ctrl+c 退出

  • -x 表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是 1。

条件表达式的例子

1
watch demo.MathGame primeFactors "{params[0],target}" "params[0]<0"

Q 或者 Ctrl+c 退出

  • 只有满足条件的调用,才会有响应。
  • watch-express 单个值可以不加’{}’,多个值需要加’{a,b,c}’。
  • condition-express 不能加’{}’,可以使用逗号分隔子表达式,取表达式最后一个值来判断。
  • 如果 watch 的方法存在同名的其它重载方法,可以通过下面的办法进行过滤:
  • 根据参数类型进行过滤
1
watch demo.MathGame primeFactors '{params, params[0].class.name}' 'params[0].class.name == "java.lang.Integer"'

Q 或者 Ctrl+c 退出

  • 根据参数个数进行过滤
1
watch demo.MathGame primeFactors '{params, params.length}' 'params.length==1'

Q 或者 Ctrl+c 退出

观察异常信息的例子

1
watch demo.MathGame primeFactors "{params[0],throwExp}" -e -x 2

Q 或者 Ctrl+c 退出

  • -e 表示抛出异常时才触发
  • express 中,表示异常信息的变量是throwExp

根据异常类型或者 message 进行过滤:

1
watch demo.MathGame primeFactors '{params, throwExp}' '#msg=throwExp.toString(), #msg.contains("IllegalArgumentException")' -e -x 2

Q 或者 Ctrl+c 退出

按照耗时进行过滤

1
watch demo.MathGame primeFactors '{params, returnObj}' '#cost>200' -x 2

Q 或者 Ctrl+c 退出

  • #cost>200 (单位是ms ) 表示只有当耗时大于 200ms 时才会输出,过滤掉执行时间小于 200ms 的调用

观察当前对象中的属性

1
watch demo.MathGame primeFactors 'target'

Q 或者 Ctrl+c 退出

如果想查看方法运行前后,当前对象中的属性,可以使用target 关键字,代表当前对象
然后使用target.field_name 访问当前对象的某个属性

1
watch demo.MathGame primeFactors 'target.illegalArgumentCount'

Q 或者 Ctrl+c 退出

wc 命令

按行统计输出结果

通过 jad demo.MathGame main 输出 demo.MathGame 类的 main 方法

计算输出结果行数 jad demo.MathGame main | wc -l

memory 命令

查看 JVM 内存信息。

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ memory
Memory used total max usage
heap 32M 256M 4096M 0.79%
g1_eden_space 11M 68M -1 16.18%
g1_old_gen 17M 184M 4096M 0.43%
g1_survivor_space 4M 4M -1 100.00%
nonheap 35M 39M -1 89.55%
codeheap_'non-nmethods' 1M 2M 5M 20.53%
metaspace 26M 27M -1 96.88%
codeheap_'profiled_nmethods' 4M 4M 117M 3.57%
compressed_class_space 2M 3M 1024M 0.29%
codeheap_'non-profiled_nmethods' 685K 2496K 120032K 0.57%
mapped 0K 0K - 0.00%
direct 48M 48M - 100.00%

auth 命令

提示

验证当前会话

配置用户名和密码

在 attach 时,可以在命令行指定密码。比如:

1
java -jar arthas-boot.jar --password ppp
  • 可以通过 --username 选项来指定用户,默认值是arthas

  • 也可以在 arthas.properties 里中配置 username/password。命令行的优先级大于配置文件。

  • 如果只配置username,没有配置password,则会生成随机密码,打印在~/logs/arthas/arthas.log

    1
    Using generated security password: 0vUBJpRIppkKuZ7dYzYqOKtranj4unGh

本地连接不鉴权

默认情况下,在arthas.properties文件里有配置:

1
arthas.localConnectionNonAuth=true

当配置密码时,使用本地连接,也不需要鉴权。默认配置值是 true,方便本地连接使用。只有远程连接时,才需要鉴权。

在 telnet console 里鉴权

连接到 arthas 后,直接执行命令会提示需要鉴权:

1
2
[arthas@37430]$ help
Error! command not permitted, try to use 'auth' command to authenticates.

使用auth命令来鉴权,成功之后可以执行其它命令。

1
2
[arthas@37430]$ auth ppp
Authentication result: true
  • 可以通过 --username 选项来指定用户,默认值是arthas

Web console 密码验证

打开浏览器,会有弹窗提示需要输入 用户名 和 密码。

成功之后,则可以直接连接上 web console。

HTTP API 验证

Authorization Header 方式(推荐)

Arthas 采用的是 HTTP 标准的 Basic Authorization,客户端请求时增加对应的 header 即可。

例如,用户名是:admin,密码是 admin,则组合为字符串: admin:admin,base64 结果是: YWRtaW46YWRtaW4=,则 HTTP 请求增加Authorization header:

1
2
3
curl 'http://localhost:8563/api' \
-H 'Authorization: Basic YWRtaW46YWRtaW4=' \
--data-raw '{"action":"exec","command":"version"}'

URL 参数传递方式

为了方便各种特殊情况,支持了以 parameters 方式传递 username 和 password。比如:

1
2
curl 'http://localhost:8563/api?password=admin' \
--data-raw '{"action":"exec","command":"version"}'

查看 JVM 信息

下面介绍 Arthas 里查看 JVM 信息的命令。

sysprop

sysprop 可以打印所有的 System Properties 信息。

提示

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

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
USAGE:
sysprop [-h] [property-name] [property-value]

SUMMARY:
Display, and change all the system properties.

EXAMPLES:
sysprop
sysprop file.encoding
sysprop production.mode true

WIKI:
https://arthas.aliyun.com/doc/sysprop

OPTIONS:
-h, --help this help
<property-name> property name
<property-value> property value

查看所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
$ sysprop
KEY VALUE
-------------------------------------------------------------------------------------------------------------------------------------
java.runtime.name Java(TM) SE Runtime Environment
sun.boot.library.path /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib
java.vm.version 25.51-b03
user.country.format CN
gopherProxySet false
java.vm.vendor Oracle Corporation
java.vendor.url http://java.oracle.com/
path.separator :
java.vm.name Java HotSpot(TM) 64-Bit Server VM
file.encoding.pkg sun.io
user.country US
sun.java.launcher SUN_STANDARD
sun.os.patch.level unknown
java.vm.specification.name Java Virtual Machine Specification
user.dir /private/var/tmp
java.runtime.version 1.8.0_51-b16
java.awt.graphicsenv sun.awt.CGraphicsEnvironment
java.endorsed.dirs /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/endors
ed
os.arch x86_64
java.io.tmpdir /var/folders/2c/tbxwzs4s4sbcvh7frbcc7n000000gn/T/
line.separator

java.vm.specification.vendor Oracle Corporation
os.name Mac OS X
sun.jnu.encoding UTF-8
java.library.path /Users/wangtao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Libra
ry/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
sun.nio.ch.bugLevel
java.specification.name Java Platform API Specification
java.class.version 52.0
sun.management.compiler HotSpot 64-Bit Tiered Compilers
os.version 10.12.6
user.home /Users/wangtao
user.timezone Asia/Shanghai
java.awt.printerjob sun.lwawt.macosx.CPrinterJob
file.encoding UTF-8
java.specification.version 1.8
user.name wangtao
java.class.path .
java.vm.specification.version 1.8
sun.arch.data.model 64
java.home /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre
sun.java.command Test
java.specification.vendor Oracle Corporation
user.language en
awt.toolkit sun.lwawt.macosx.LWCToolkit
java.vm.info mixed mode
java.version 1.8.0_51
java.ext.dirs /Users/wangtao/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.
8.0_51.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library
/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.boot.class.path /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resour
ces.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/li
b/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/l
ib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/H
ome/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Content
s/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Conte
nts/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jd
k/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.
jdk/Contents/Home/jre/classes
java.vendor Oracle Corporation
file.separator /
java.vendor.url.bug http://bugreport.sun.com/bugreport/
sun.cpu.endian little
sun.io.unicode.encoding UnicodeBig
sun.cpu.isalist

查看单个属性

1
2
$ sysprop java.version
java.version=1.8.0_51

修改单个属性

1
2
3
4
5
$ sysprop user.country
user.country=US
$ sysprop user.country CN
Successfully changed the system property.
user.country=CN

sysenv

sysenv 命令可以获取到环境变量。和sysprop 命令类似。

提示

查看当前 JVM 的环境属性( System Environment Variables )

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
USAGE:
sysenv [-h] [env-name]

SUMMARY:
Display the system env.

EXAMPLES:
sysenv
sysenv USER

WIKI:
https://arthas.aliyun.com/doc/sysenv

OPTIONS:
-h, --help this help
<env-name> env name

查看所有环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ sysenv
KEY VALUE
----------------------------------------------------------------------------------------------------------------------------
PATH /Users/admin/.sdkman/candidates/visualvm/current/bin:/Users/admin/.sdkman/candidates/ja
va/current/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/Wireshark.app/Contents/
MacOS
SDKMAN_VERSION 5.7.3+337
JAVA_HOME /Users/admin/.sdkman/candidates/java/current
JAVA_MAIN_CLASS_65244 demo.MathGame
TERM xterm-256color
LANG zh_CN.UTF-8
AUTOJUMP_SOURCED 1
COLORTERM truecolor
LOGNAME admin
XPC_SERVICE_NAME 0
PWD /Users/admin/code/ali/arthas/demo
TERM_PROGRAM_VERSION 3.2.5
_ /Users/admin/.sdkman/candidates/java/current/bin/java
SHELL /bin/bash
TERM_PROGRAM iTerm.app
SDKMAN_PLATFORM Darwin
USER admin
ITERM_PROFILE Default
TMPDIR /var/folders/0r/k561bkk917gg972stqclbz9h0000gn/T/
XPC_FLAGS 0x0
TERM_SESSION_ID w0t4p0:60BC264D-9649-42AC-A7E4-AF85B69F93F8
__CF_USER_TEXT_ENCODING 0x1F5:0x19:0x34
Apple_PubSub_Socket_Ren /private/tmp/com.apple.launchd.DwmmjSQsll/Render
der
COLORFGBG 7;0
HOME /Users/admin
SHLVL 1
AUTOJUMP_ERROR_PATH /Users/admin/Library/autojump/errors.log

查看单个环境变量

1
2
$ sysenv USER
USER=admin

JVM

jvm 命令会打印出JVM 的各种详细信息。

提示

查看当前 JVM 信息

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
$ jvm
RUNTIME
--------------------------------------------------------------------------------------------------------------
MACHINE-NAME 37@ff267334bb65
JVM-START-TIME 2020-07-23 07:50:36
MANAGEMENT-SPEC-VERSION 1.2
SPEC-NAME Java Virtual Machine Specification
SPEC-VENDOR Oracle Corporation
SPEC-VERSION 1.8
VM-NAME Java HotSpot(TM) 64-Bit Server VM
VM-VENDOR Oracle Corporation
VM-VERSION 25.201-b09
INPUT-ARGUMENTS []
CLASS-PATH demo-arthas-spring-boot.jar
BOOT-CLASS-PATH /usr/lib/jvm/java-8-oracle/jre/lib/resources.jar:/usr/lib/jvm/java-8-oracle/j
re/lib/rt.jar:/usr/lib/jvm/java-8-oracle/jre/lib/sunrsasign.jar:/usr/lib/jvm/
java-8-oracle/jre/lib/jsse.jar:/usr/lib/jvm/java-8-oracle/jre/lib/jce.jar:/us
r/lib/jvm/java-8-oracle/jre/lib/charsets.jar:/usr/lib/jvm/java-8-oracle/jre/l
ib/jfr.jar:/usr/lib/jvm/java-8-oracle/jre/classes
LIBRARY-PATH /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib

--------------------------------------------------------------------------------------------------------------
CLASS-LOADING
--------------------------------------------------------------------------------------------------------------
LOADED-CLASS-COUNT 7529
TOTAL-LOADED-CLASS-COUNT 7529
UNLOADED-CLASS-COUNT 0
IS-VERBOSE false

--------------------------------------------------------------------------------------------------------------
COMPILATION
--------------------------------------------------------------------------------------------------------------
NAME HotSpot 64-Bit Tiered Compilers
TOTAL-COMPILE-TIME 14921(ms)

--------------------------------------------------------------------------------------------------------------
GARBAGE-COLLECTORS
--------------------------------------------------------------------------------------------------------------
PS Scavenge name : PS Scavenge
[count/time (ms)] collectionCount : 7
collectionTime : 68

PS MarkSweep name : PS MarkSweep
[count/time (ms)] collectionCount : 1
collectionTime : 47

--------------------------------------------------------------------------------------------------------------
MEMORY-MANAGERS
--------------------------------------------------------------------------------------------------------------
CodeCacheManager Code Cache

Metaspace Manager Metaspace
Compressed Class Space

Copy Eden Space
Survivor Space

MarkSweepCompact Eden Space
Survivor Space
Tenured Gen


--------------------------------------------------------------------------------------------------------------
MEMORY
--------------------------------------------------------------------------------------------------------------
HEAP-MEMORY-USAGE init : 268435456(256.0 MiB)
[memory in bytes] used : 18039504(17.2 MiB)
committed : 181403648(173.0 MiB)
max : 3817865216(3.6 GiB)

NO-HEAP-MEMORY-USAGE init : 2555904(2.4 MiB)
[memory in bytes] used : 33926216(32.4 MiB)
committed : 35176448(33.5 MiB)
max : -1(-1 B)

--------------------------------------------------------------------------------------------------------------
OPERATING-SYSTEM
--------------------------------------------------------------------------------------------------------------
OS Linux
ARCH amd64
PROCESSORS-COUNT 3
LOAD-AVERAGE 29.53
VERSION 4.15.0-52-generic

--------------------------------------------------------------------------------------------------------------
THREAD
--------------------------------------------------------------------------------------------------------------
COUNT 30
DAEMON-COUNT 24
PEAK-COUNT 31
STARTED-COUNT 36
DEADLOCK-COUNT 0

--------------------------------------------------------------------------------------------------------------
FILE-DESCRIPTOR
--------------------------------------------------------------------------------------------------------------
MAX-FILE-DESCRIPTOR-COUNT 1048576
OPEN-FILE-DESCRIPTOR-COUNT 100
Affect(row-cnt:0) cost in 88 ms.

THREAD 相关

  • COUNT: JVM 当前活跃的线程数
  • DAEMON-COUNT: JVM 当前活跃的守护线程数
  • PEAK-COUNT: 从 JVM 启动开始曾经活着的最大线程数
  • STARTED-COUNT: 从 JVM 启动开始总共启动过的线程次数
  • DEADLOCK-COUNT: JVM 当前死锁的线程数

文件描述符相关

  • MAX-FILE-DESCRIPTOR-COUNT:JVM 进程最大可以打开的文件描述符数
  • OPEN-FILE-DESCRIPTOR-COUNT:JVM 当前打开的文件描述符数

Tips

为了更好使用 Arthas,下面先介绍 Arthas 里的一些使用技巧。

help

Arthas 里每一个命令都有详细的帮助信息。可以用-h 来查看。帮助信息里有EXAMPLESWIKI 链接。

比如:

1
sysprop -h

自动补全

Arthas 支持丰富的自动补全功能,在使用有疑惑时,可以输入Tab 来获取更多信息。

比如输入 sysprop java. 之后,再输入Tab ,会补全出对应的 key。

readline 的快捷键支持

Arthas 支持常见的命令行快捷键,比如Ctrl + A 跳转行首,Ctrl + E 跳转行尾。

更多的快捷键可以用 keymap 命令查看。

历史命令的补全

如果想再执行之前的命令,可以在输入一半时,按Up/↑ 或者 Ddown/↓ ,来匹配到之前的命令。

比如之前执行过sysprop java.version ,那么在输入sysprop ja 之后,可以输入Up/↑ ,就会自动补全为sysprop java.version

如果想查看所有的历史命令,也可以通过 history 命令查看到。

pipeline

Arthas 支持在 pipeline 之后,执行一些简单的命令,比如:

1
2
sysprop | grep java
sysprop | wc -l

Sc/sm 查看已加载的类

下面介绍 Arthas 里查找已加载类的命令。

Sc

sc 命令可以查找到所有 JVM 已经加载到的类。

如果搜索的是接口,还会搜索所有的实现类。比如查看所有的Filter 实现类:

1
sc javax.servlet.Filter

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

1
sc -d javax.servlet.Filter

sc 支持通配,比如搜索所有的StringUtils

1
sc *StringUtils

sm

提示

查看已加载类的方法信息

“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息。

sm 命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。

参数说明

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

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ sm java.lang.String
java.lang.String-><init>
java.lang.String->equals
java.lang.String->toString
java.lang.String->hashCode
java.lang.String->compareTo
java.lang.String->indexOf
java.lang.String->valueOf
java.lang.String->checkBounds
java.lang.String->length
java.lang.String->isEmpty
java.lang.String->charAt
java.lang.String->codePointAt
java.lang.String->codePointBefore
java.lang.String->codePointCount
java.lang.String->offsetByCodePoints
java.lang.String->getChars
java.lang.String->getBytes
java.lang.String->contentEquals
java.lang.String->nonSyncContentEquals
java.lang.String->equalsIgnoreCase
java.lang.String->compareToIgnoreCase
java.lang.String->regionMatches
java.lang.String->startsWith
java.lang.String->endsWith
java.lang.String->indexOfSupplementary
java.lang.String->lastIndexOf
java.lang.String->lastIndexOfSupplementary
java.lang.String->substring
java.lang.String->subSequence
java.lang.String->concat
java.lang.String->replace
java.lang.String->matches
java.lang.String->contains
java.lang.String->replaceFirst
java.lang.String->replaceAll
java.lang.String->split
java.lang.String->join
java.lang.String->toLowerCase
java.lang.String->toUpperCase
java.lang.String->trim
java.lang.String->toCharArray
java.lang.String->format
java.lang.String->copyValueOf
java.lang.String->intern
Affect(row-cnt:44) cost in 1342 ms.
1
2
3
4
5
6
7
8
9
10
$ sm -d java.lang.String toString
declaring-class java.lang.String
method-name toString
modifier public
annotation
parameters
return java.lang.String
exceptions

Affect(row-cnt:1) cost in 3 ms.

Jad

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

1
jad com.example.demo.arthas.user.UserController

通过--source-only 参数可以只打印出在反编译的源代码:

1
jad --source-only com.example.demo.arthas.user.UserController

OGNL

OGNL 一般语法

索引

  • 数组和列表的索引,可以使用 array["length"]

  • JavaBeans 索引属性

    如一个 JavaBeans 有以下四个重载方法:

    1
    2
    3
    4
    public PropertyType[] getPropertyName();
    public void setPropertyName(PropertyType[] anArray);
    public PropertyType getPropertyName(int index);
    public void setPropertyName(int index, PropertyType value);

    1
    someProperty[2]

    等价于 Java 代码的

    1
    getPropertyName(2)
1
ognl "{1,2,3,4}[0]"

通过上面命令可以获取到列表的第一个元素

变量引用

使用 # 在 OGNL 中定义临时变量,他们全局可见,此外表达式计算的每一步结果都保存在变量 this 中:

1
ognl "{10,20,30}[0].(#this > 5 ? #this*2 : #this+10)"

上面命令通过获取列表的第一个元素进行判断,如果大于 5 则乘以 2 反之则加 10

方法调用

1
method( ensureLoaded(), name )

注意:

  • OGNL 是运行时调用,因此没有任何静态类型的信息可以参考,所以如果解析到有多个匹配的方法,则任选其中一个方法调用
  • 常量 null 可以匹配所有的非原始类型的对象
1
ognl "{1,2,3,4}.size()"

通过上面命令可以调用 ArrayListsize() 方法获取到 ArrayList 的大小

复杂链式表达式

1
headline.parent.(ensureLoaded(), name)

等价于

1
2
headline.parent.ensureLoaded(), headline.parent.name
ognl "@java.lang.System@out.(print('Hello '), print('world\n'))"

运行上面这个命令后,你可以在 Tab1 的终端看到 Hello world 的输出。

集合操作

新建列表

1
ognl "1 in {2, 3}"

上面这条命令判断 1 是否在列表 [2, 3] 中。

新建原生数组

1
ognl "new int[] {1, 2, 3}"

指定长度

1
ognl "new int[9]"

新建 Maps

新建普通 Map

1
ognl "#{ 'foo': 'foo value', 'bar': 'bar value' }"

新建特定类型 Map

1
ognl "#@java.util.HashMap@{ 'foo': 'foo value', 'bar': 'bar value' }"

集合的投影

OGNL 把对针对集合上的每个元素调用同一个方法并返回新的集合的行为称之为“投影”。

1
ognl "{1, 2, 3}.{#this*2}"

查找集合元素

  • 查找所有匹配的元素
    ognl "{1024, 'Hello world!', true, 2048}.{? #this instanceof Number}"
  • 查找第一个匹配的元素
    ognl "{1024, 'Hello world!', true, 2048}.{^ #this instanceof Number}"
  • 查找最后一个匹配的元素
    ognl "{1024, 'Hello world!', true, 2048}.{$ #this instanceof Number}"

集合的虚拟属性

OGNL 定义了一些特定的集合属性,含义与相应的 Java 集合方法完全等价。

  • Collections
    • size 集合大小
    • isEmpty 集合为空时返回 true
  • List
    • iterator 返回 List 的 iterator
  • Map
    • keys 返回 Map 的所有 Key 值
    • values 返回 Map 的所有 Value 值
  • Set
    • iterator 返回 Set 的 iterator

构造函数

java.lang 包下的所有类的构造函数都要用类的权限定名称。

1
ognl "new java.util.ArrayList()"

静态方法

1
ognl -x 3 '@java.lang.Math@sqrt(9.0D)'

静态属性

1
ognl -x 3 '@java.io.File@separator'

伪 lambda 表达式

1
ognl "#fact = :[#this<=1? 1 : #this*#fact(#this-1)], #fact(3)"

该命令实现了一个 lambda 递归实现了一个阶乘函数,并求 3 的阶乘。

补充

更多详细语法请查看官方文档

Arthas-OGNL

在 Arthas 里,有一个单独的 ognl 命令,可以动态执行代码。

调用 static 函数

1
ognl '@java.lang.System@out.println("hello ognl")'

查找 UserController 的 ClassLoader

1
sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。
如果你使用-c ,你需要手动输入由上述命令获取到的 hashcode:-c <hashcode>
对于只有唯一实例的 ClassLoader 可以通过--classLoaderClass 指定 class name,使用起来更加方便:
--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode> 是动态变化的。

获取静态类的静态字段

获取CRBCMsgAdapterController 类里的logger 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
[arthas@9012]$ ognl -c 18b4aac2 @com.crb.adaptor.controller.CRBCMsgAdapterController@logger
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.crb.adaptor.controller.CRBCMsgAdapterController],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[Logger[com.crb.adaptor.controller]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[arthas@9012]$ ognl c 18b4aacc2 -x 2 @com.crb.adaptor.controller.CRBCMsgAdapterController@logger
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.crb.adaptor.controller.CRBCMsgAdapterController],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[com.crb.adaptor.controller],
level=null,
effectiveLevelInt=@Integer[20000],
parent=@Logger[Logger[com.crb.adaptor]],
childrenList=@CopyOnWriteArrayList[isEmpty=false;size=1],
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[
DEFAULT_PACKAGING_DATA=@Boolean[false],
root=@Logger[Logger[ROOT]],
size=@Integer[416],
noAppenderWarning=@Integer[2],
loggerContextListenerList=@ArrayList[isEmpty=false;size=1],
loggerCache=@ConcurrentHashMap[isEmpty=false;size=416],
loggerContextRemoteView=@LoggerContextVO[LoggerContextVO{name='default', propertyMap={PORT=8610, APPLICATION_NAME=core-tf-application}, birthTime=1704438833539}],
turboFilterList=@TurboFilterList[isEmpty=true;size=0],
packagingDataEnabled=@Boolean[false],
maxCallerDataDepth=@Integer[8],
resetCount=@Integer[2],
frameworkPackages=@ArrayList[isEmpty=true;size=0],
birthTime=@Long[1704438833539],
name=@String[default],
sm=@BasicStatusManager[ch.qos.logback.core.BasicStatusManager@2eb37a8d],
propertyMap=@HashMap[isEmpty=false;size=2],
objectMap=@HashMap[isEmpty=false;size=7],
configurationLock=@LogbackLock[ch.qos.logback.core.spi.LogbackLock@1db45c81],
scheduledExecutorService=@ScheduledThreadPoolExecutor[java.util.concurrent.ScheduledThreadPoolExecutor@ac21a5d[Running, pool size = 8, active threads = 0, queued tasks = 1, completed tasks = 28]],
scheduledFutures=@ArrayList[isEmpty=false;size=1],
lifeCycleManager=@LifeCycleManager[ch.qos.logback.core.LifeCycleManager@74197f30],
started=@Boolean[false],
],
]

执行多行表达式,赋值给临时变量,返回一个 List

1
2
3
4
5
$ 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],
]

更多

在 Arthas 里ognl 表达式是很重要的功能,在很多命令里都可以使用ognl 表达式。

一些更复杂的用法,可以参考:


Options

在 Arthas 里有一些开关,可以通过 options 命令来查看 。

查看所有的 options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ options
LEVEL TYPE NAME VALUE SUMMARY DESCRIPTION
-------------------------------------------------------------------------------------------------------
0 boolea unsafe false Option to support sy This option enables to proxy functionality
n stem-level class of JVM classes. Due to serious security r
isk a JVM crash is possibly be introduced.
Do not activate it unless you are able to
manage.
1 boolea dump false Option to dump the e This option enables the enhanced classes t
n nhanced classes o be dumped to external file for further d
e-compilation and analysis.
1 boolea batch-re-tra true Option to support ba This options enables to reTransform classe
n nsform tch reTransform Clas s with batch mode.
s
2 boolea json-format false Option to support JS This option enables to format object outpu
n ON format of object t with JSON when -x option selected.
output
1 boolea disable-sub- false Option to control in This option disable to include sub class w
n class clude sub class when hen matching class.
class matching
1 boolea support-defa true Option to control in This option disable to include default met
n ult-method clude default method hod in interface when matching class.
in interface when c
lass matching
1 boolea save-result false Option to print comm This option enables to save each command's
n and's result to log result to log file, which path is ${user.
file home}/logs/arthas-cache/result.log.
2 String job-timeout 1d Option to job timeou This option setting job timeout,The unit c
t an be d, h, m, s for day, hour, minute, se
cond. 1d is one day in default
1 boolea print-parent true Option to print all This option enables print files in parent
n -fields fileds in parent cla class, default value true.
ss
1 boolea verbose false Option to print verb This option enables print verbose informat
n ose information ion, default value false.
1 boolea strict true Option to strict mod By default, strict mode is true, not allow
n e ed to set object properties. Want to set o
bject properties, execute `options strict
false`

参数说明

名称 默认值 描述
unsafe false 是否支持对系统级别的类进行增强,打开该开关可能导致把 JVM 搞挂,请慎重选择!
dump false 是否支持被增强了的类 dump 到外部文件中,如果打开开关,class 文件会被 dump 到/${application working dir}/arthas-class-dump/目录下,具体位置详见控制台输出
batch-re-transform true 是否支持批量对匹配到的类执行 retransform 操作
json-format false 是否支持 json 化的输出
disable-sub-class false 是否禁用子类匹配,默认在匹配目标类的时候会默认匹配到其子类,如果想精确匹配,可以关闭此开关
support-default-method true 是否支持匹配到 default method, 默认会查找 interface,匹配里面的 default method。参考 #1105在新窗口打开
save-result false 是否打开执行结果存日志功能,打开之后所有命令的运行结果都将保存到~/logs/arthas-cache/result.log
job-timeout 1d 异步后台任务的默认超时时间,超过这个时间,任务自动停止;比如设置 1d, 2h, 3m, 25s,分别代表天、小时、分、秒
print-parent-fields true 是否打印在 parent class 里的 filed
verbose false 是否打印更多详细信息
strict true 是否启用 strict 模式

获取单个option 的值

1
2
3
4
5
6
7
$ options json-format
LEVEL TYPE NAME VALUE SUMMARY DESCRIPTION
--------------------------------------------------------------------------------------------
2 bool json-format false Option to support This option enables to format object
ean JSON format of obj output with JSON when -x option selec
ect output ted.

提示

默认情况下 json-format 为 false,如果希望 watch / tt 等命令结果以 json 格式输出,则可以设置 json-format 为 true。

设置指定的 option

例如,想打开执行结果存日志功能,输入如下命令即可:

1
2
3
4
$ options save-result true
NAME BEFORE-VALUE AFTER-VALUE
----------------------------------------
save-result false true

允许增强 JDK 的类

默认情况下unsafe 为 false,即 watch/trace 等命令不会增强 JVM 的类,即java.* 下面的类。

如果想增强 JVM 里的类,可以执行 options unsafe true ,设置unsafe 为 true。

以 JSON 格式打印对象

options json-format 可以看到当前的 json-format 为 false
运行 ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}' 得到的结果并不是 JSON 格式

如果希望输出 JSON 格式,可以使用 options json-format true 开启,开启后再运行 ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}' 这是可以看到输出的格式已经转变为 JSON 格式。


案例:排查函数调用异常

现象

目前,访问 /user/0 ,会返回 500 异常:

但请求的具体参数,异常栈是什么呢?

查看 UserController 的 参数/异常

在 Arthas 里执行:

1
watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
  1. 第一个参数是类名,支持通配
  2. 第二个参数是函数名,支持通配

访问 /user/0,watch 命令会打印调用的参数和异常

1
2
3
4
5
6
7
8
[arthas@5728]$ 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 172 ms, listenerId: 1
method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
ts=2024-01-05 16:31:06; [cost=6.6047ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@IllegalArgumentException[java.lang.IllegalArgumentException: id < 1],
]

可以看到实际抛出的异常是IllegalArgumentException

可以输入 Q 或者 Ctrl+C 退出 watch 命令。

如果想把获取到的结果展开,可以用-x 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[arthas@6392]$ watch  com.example.demo.arthas.user.UserController  * '{params, throwExp}' -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 119 ms, listenerId: 1
method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
ts=2024-01-05 16:38:32; [cost=6.8937ms] result=@ArrayList[
@Object[][
@Integer[0],
],
java.lang.IllegalArgumentException: id < 1
at com.example.demo.arthas.user.UserController.findUserById(UserController.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
......
]

返回值表达式

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

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

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

1
watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'

更多参考:https://arthas.aliyun.com/doc/advice-class.html

条件表达式

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

1
watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'

当访问 /user/1 时,watch 命令没有输出

当访问 /user/101 时,watch 会打印出结果。

1
2
3
4
5
6
7
8
[arthas@6392]$ 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: 2
method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
ts=2024-01-05 16:46:59; [cost=0.5729ms] result=@User[
id=@Integer[101],
name=@String[name101],
]

当异常时捕获

watch 命令支持-e 选项,表示只捕获抛出异常时的请求:

1
watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e

当访问/user/0时,watch会打印异常信息。

1
2
3
4
5
6
7
8
[arthas@6392]$ watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 38 ms, listenerId: 6
method=com.example.demo.arthas.user.UserController.findUserById location=AtExceptionExit
ts=2024-01-05 16:57:40; [cost=1.197ms] result=@ArrayList[
@Integer[0],
@IllegalArgumentException[java.lang.IllegalArgumentException: id < 1],
]

按照耗时进行过滤

watch 命令支持按请求耗时进行过滤,比如:

1
watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'

案例:热更新代码

下面介绍通过jad /mc /redefine 命令实现动态更新代码的功能。

目前,访问 /user/0 ,会返回 500 异常:

下面通过热更新代码,修改这个逻辑。

jad 反编译 UserController

1
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java

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

再打开一个终端于 Tab 3 ,然后在 Tab3 里用 sed 来编辑/tmp/UserController.java

1
sed -i 's/throw new IllegalArgumentException("id < 1")/return new User(id, "name" + id)/g' /tmp/UserController.java

使用 cat 命令查看修改后的内容:

1
cat /tmp/UserController.java

比如当 user id 小于 1 时,也正常返回,不抛出异常:

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

mc (Memory Compiler) 命令来编译加载 UserController, 可以通过 -c 指定 classLoaderHash 或者 –classLoaderClass 参数指定 ClassLoader,这里为了操作连贯性使用 classLoaderClass。

查询 UserController 类加载器

sc 查找加载 UserController 的 ClassLoader

回到 Tab 2 里运行 sc -d *UserController | grep classLoaderHash

1
2
[arthas@6392]$ sc -d *UserController | grep classLoaderHash
classLoaderHash 38af3868

classloader 查询类加载器名称

classloader -l 查询所有的类加载器列表

1
2
3
4
5
6
7
8
9
10
11
[arthas@6392]$ classloader  -l
name loadedCount hash parent
BootstrapClassLoader 3221 null null
com.taobao.arthas.agent.ArthasClassloader@384abb78 2630 384abb78 sun.misc.Launcher$ExtClassLoade
r@a09ee92
org.springframework.boot.loader.LaunchedURLClassLoader@38af3868 4591 38af3868 sun.misc.Launcher$AppClassLoade
r@5c647e05
sun.misc.Launcher$AppClassLoader@5c647e05 45 5c647e05 sun.misc.Launcher$ExtClassLoade
r@a09ee92
sun.misc.Launcher$ExtClassLoader@a09ee92 19 a09ee92 null
Affect(row-cnt:5) cost in 7 ms.

UserController classLoaderHash 值对应的类加载器为 org.springframework.boot.loader.LaunchedURLClassLoader

mc 编译加载 UserController

保存到 /tmp/UserController.java 之后可以使用 mc (Memory Compiler) 命令来编译

mc 指定 classloader 编译 UserController

1
2
3
4
[arthas@3880]$ mc -c 33c7353a /tmp/UserController.java  -d /tmp/
Memory compiler output:
D:\tmp\com\example\demo\arthas\user\UserController.class
Affect(row-cnt:1) cost in 4442 ms.

redefine

再使用redefine 命令重新加载新编译好的UserController.class

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

热修改代码结果

redefine 成功之后,再次访问 /user/0,结果是:

1
2
3
4
{
"id": 0,
"name": "name0"
}

案例:Arthas 后台异步任务

arthas 中的后台异步任务,使用了仿 linux 系统任务相关的命令。

使用 & 在后台执行任务,任务输出重定向

可通过 > 或者 >> 将任务输出结果输出到指定的文件中,可以和 & 一起使用,实现 arthas 命令的后台异步任务。

当前我们需要排查一个问题,但是这个问题的出现时间不能确定,那我们就可以把检测命令挂在后台运行,并将保存到输出日志,如下命令:

1
watch com.example.demo.arthas.user.UserController * '{params, throwExp}' 'throwExp != null' >> a.log &

这时命令在后台执行,可以在 console 中继续执行其他命令。

之后我们去访问:/user/0

然后使用 cat a.log 可以看到我们刚刚访问的 URL 抛出了一个异常

通过 jobs 查看任务

如果希望查看当前有哪些 arthas 任务在执行,可以执行 jobs 命令,执行结果如下

1
2
3
4
5
6
7
[arthas@3880]$ jobs
[4]*
Running watch com.example.demo.arthas.user.UserController * '{params, throwExp != null}' >> a.log &
execution count : 1
start time : Sat Jan 06 09:36:28 CST 2024
timeout date : Sun Jan 07 09:36:28 CST 2024
session : 4251ad90-2951-4d33-8da1-2d241a3e0a0f (current)

可以看到目前有一个后台任务在执行

job id 是 4, * 表示此 job 是当前 session 创建(生命周期默认为一天)

状态是 Running

execution count 是执行次数,从启动开始已经执行了 1 次

timeout date 是超时的时间,到这个时间,任务将会自动超时退出。

停止命令、切换前后台

异步执行的命令,如果希望停止,可执行 kill, 希望命令转到前台、后台继续执行 fg、bg 命令。

kill掉job id 是 4 的任务:

1
2
3
[arthas@3880]$ kill 4
kill job 4 success
[arthas@3880]$ jobs

注意事项

  • 最多同时支持 8 个命令使用重定向将结果写日志
  • 请勿同时开启过多的后台异步命令,以免对目标 JVM 性能造成影响
  • 如果不想停止 arthas,继续执行后台任务,可以执行 quit 退出 arthas 控制台(stop 会停止 arthas 服务)

案例:动态更新应用 Logger Level

在这个案例里,动态修改应用的 Logger Level。

使用 sc 查找 UserController 的 ClassLoader

1
sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。
如果你使用-c ,你需要手动输入 hashcode:-c <hashcode>
对于只有唯一实例的 ClassLoader 可以通过--classLoaderClass 指定 class name,使用起来更加方便:
--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode> 是动态变化的。

使用 ognl

用 ognl 获取 logger

1
2
3
4
5
6
7
8
9
10
11
12
13
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@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 里来的。

查看root logger Lever:

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

单独设置 UserController 的 logger level

1
2
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
null

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

1
2
3
4
5
6
7
8
9
10
11
12
13
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@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

通过获取 root logger,可以修改全局的 logger level:

1
2
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
null

运行下面命令可以看到 ROOT 的 level 已经修改为了 DEBUG

1
2
3
4
5
6
7
8
9
10
11
12
13
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@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@452a4e77],
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]

使用 logger

使用 logger 命令可以相较于 ognl 更加便捷的实现 logger level 的动态设置。

使用 logger 命令获取对应 logger 信息

1
2
3
4
5
6
7
8
9
[arthas@3880]$ logger --name  com.example.demo.arthas.user.UserController
name com.example.demo.arthas.user.UserController
class ch.qos.logback.classic.Logger
classLoader org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a
classLoaderHash 33c7353a
level DEBUG
effectiveLevel DEBUG
additivity true
codeSource jar:file:/D:/arthas/arthas-packaging-3.7.1-bin/demo-arthas-spring-boot.jar!/BOOT-INF/lib/logback-classic-1.1.11.jar!/

单独设置 UserController 的 logger level

1
2
[arthas@3880]$ logger --name com.example.demo.arthas.user.UserController --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --level WARN
Update logger level success.

再次获取对应 logger 信息,可以发现已经是 WARN 了:

1
2
3
4
5
6
7
8
9
[arthas@3880]$ logger --name com.example.demo.arthas.user.UserController
name com.example.demo.arthas.user.UserController
class ch.qos.logback.classic.Logger
classLoader org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a
classLoaderHash 33c7353a
level WARN
effectiveLevel WARN
additivity true
codeSource jar:file:/D:/arthas/arthas-packaging-3.7.1-bin/demo-arthas-spring-boot.jar!/BOOT-INF/lib/logback-classic-1.1.11.jar!/

修改 logback 的全局 logger level

1
2
[arthas@3880]$ logger --name ROOT --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --level ERROR
Update logger level success.

运行下面命令可以看到 ROOT 的 level 已经修改为了 ERROR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[arthas@3880]$ logger  --name ROOT --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader
name ROOT
class ch.qos.logback.classic.Logger
classLoader org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a
classLoaderHash 33c7353a
level ERROR
effectiveLevel ERROR
additivity true
codeSource jar:file:/D:/arthas/arthas-packaging-3.7.1-bin/demo-arthas-spring-boot.jar!/BOOT-INF/lib/logback-classic-1.1.11.jar!/
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a
classLoaderHash 33c7353a
target System.out

案例:排查 logger 冲突问题

在这个案例里,展示排查 logger 冲突的方法。

查找 UserController 的 ClassLoader

1
sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash

注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。
如果你使用-c ,你需要手动输入 hashcode:-c <hashcode>
对于只有唯一实例的 ClassLoader 可以通过--classLoaderClass 指定 class name,使用起来更加方便:
--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode> 是动态变化的。

确认应用使用的 logger 系统

UserController 为例,它使用的是 slf4j api,但实际使用到的 logger 系统是 logback。

1
2
3
4
5
6
7
8
9
10
11
12
13
[arthas@3880]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@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[INFO],
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]],
]

获取 logback 实际加载的配置文件

1
2
3
4
5
6
7
8
9
[arthas@3880]$ ognl --classLoaderClass  org.springframework.boot.loader.LaunchedURLClassLoader  '#map1=@org.slf4j.LoggerFactory@getLogger("root").loggerContext.objectMap, #map1.get("CONFIGURATION_WATCH_LIST")'
@ConfigurationWatchList[
mainURL=@URL[jar:file:/D:/arthas/arthas-packaging-3.7.1-bin/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/logback-spring.xml],
fileWatchList=@ArrayList[isEmpty=true;size=0],
lastModifiedList=@ArrayList[isEmpty=true;size=0],
noContextWarning=@Integer[0],
context=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
declaredOrigin=@ConfigurationWatchList[ch.qos.logback.core.joran.spi.ConfigurationWatchList@5149876],
]

使用classloader 命令查找可能存在的 logger 配置文件

1
2
3
4
[arthas@3880]$ classloader --classLoaderClass  org.springframework.boot.loader.LaunchedURLClassLoader -r logback-spring.xml
jar:file:/D:/arthas/arthas-packaging-3.7.1-bin/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/logback-spring.xml

Affect(row-cnt:1) cost in 1 ms.

可以知道加载的配置的具体来源。

可以尝试加载容易冲突的文件:

1
2
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r logback.xml
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r log4j.properties

案例:获取 Spring Context

在这个案例里,展示获取 spring context,再获取 bean,然后调用函数。

使用 tt 命令获取到 spring context

tt 即 TimeTunnel,它可以记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。

1
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod

访问:/user/1

可以看到tt 命令捕获到了请求

1
2
3
4
5
6
[arthas@3880]$ tt -t  org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 50 ms, listenerId: 2
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1000 2024-01-06 15:47:59 4.1345 true false 0x369e7ab5 RequestMappingHandlerAdapter invokeHandlerMethod

使用 tt 命令从调用记录里获取到 spring context

输入 Q 或者 Ctrl + C 退出上面的 tt -t 命令。
使用 tt -l 命令可以查看之前 tt 命令捕获到的请求信息。

1
tt -i 1000 -w 'target.getApplicationContext()'

获取 spring bean,并调用函数

1
2
3
[arthas@3880]$ tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()'
@String[Hello World]
Affect(row-cnt:1) cost in 4 ms.

Vmtool 获取 HelloWorldService 对象实例,并调用函数

上面的方法使用 tt 命令获取 spring bean,并调用函数,有点繁琐,更换为使用 vmtool 将会极大的简化这个流程,命令如下:

1
2
[arthas@3880]$ vmtool --action getInstances --className com.example.demo.arthas.aop.HelloWorldService --express 'instances[0].getHelloMessage()'
@String[Hello World]

案例:排查 HTTP 请求返回 401

在这个案例里,展示排查 HTTP 401 问题的技巧。

访问: /admin

结果是:

1
Something went wrong: 401 Unauthorized

我们知道401 通常是被权限管理的Filter 拦截了,那么到底是哪个Filter 处理了这个请求,返回了 401?

跟踪所有的 Filter 函数

开始 trace:

1
trace javax.servlet.Filter *

访问: /admin

可以在调用树的最深层,找到AdminFilterConfig$AdminFilter 返回了401

1
2
3
+---[3.806273ms] javax.servlet.FilterChain:doFilter()
| `---[3.447472ms] com.example.demo.arthas.AdminFilterConfig$AdminFilter:doFilter()
| `---[0.17259ms] javax.servlet.http.HttpServletResponse:sendError()

输入 Q 或者 Ctrl+C 退出 watch 命令。

通过 stack 获取调用栈

上面是通过trace 命令来获取信息,从结果里,我们可以知道通过stack 跟踪HttpServletResponse:sendError() ,同样可以知道是哪个Filter 返回了401

执行:

1
stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401'

访问: /admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[arthas@3880]$ stack  javax.servlet.http.HttpServletResponse sendError 'params[0]==401'
Press Q or Ctrl+C to abort.
Affect(class count: 3 , method count: 4) cost in 99 ms, listenerId: 4
ts=2024-01-06 16:33:24;thread_name=http-nio-80-exec-3;id=14;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@54c14576
@org.apache.catalina.connector.Response.sendError()
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:462)
at com.example.demo.arthas.AdminFilterConfig$AdminFilter.doFilter(AdminFilterConfig.java:38)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

输入 Q 或者 Ctrl+C 退出 watch 命令。

案例:理解 Spring Boot 应用的 ClassLoader 结构

下面介绍classloader 命令的功能。

先访问一个 jsp 网页,触发 jsp 的加载: 访问 hello 页面

列出所有 ClassLoader

1
2
3
4
5
6
7
8
9
[arthas@3880]$ classloader -l
name loadedCount hash parent
BootstrapClassLoader 3132 null null
com.taobao.arthas.agent.ArthasClassloader@a3df247 1810 a3df247 sun.misc.Launcher$ExtClassLoader@f6f4d33
java.net.FactoryURLClassLoader@7b8e788f 842 7b8e788f sun.misc.Launcher$AppClassLoader@3d4eac69
org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a 4759 33c7353a sun.misc.Launcher$AppClassLoader@3d4eac69
sun.misc.Launcher$AppClassLoader@3d4eac69 45 3d4eac69 sun.misc.Launcher$ExtClassLoader@f6f4d33
sun.misc.Launcher$ExtClassLoader@f6f4d33 23 f6f4d33 null
Affect(row-cnt:6) cost in 4 ms.

统计 ClassLoader 实际使用 URL 和未使用的 URL

1
classloader --url-stat

注意:基于 JVM 目前已加载的所有类统计,不代表 Unused URLs 可以从应用中删掉。因为可能将来需要从 Unused URLs 里加载类,或者需要加载 resources

列出 ClassLoader 里加载的所有类

列出上面的org.apache.jasper.servlet.JasperLoader 加载的类:

1
2
[arthas@3880]$ classloader -a --classLoaderClass  org.apache.jasper.servlet.JasperLoader | grep hello
org.apache.jsp.jsp.hello_jsp

查看类的 classloader 层次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[arthas@3880]$ sc -d org.apache.jsp.jsp.hello_jsp
class-info org.apache.jsp.jsp.hello_jsp
code-source /C:/Users/pt-tianzhencai/AppData/Local/Temp/tomcat.2859845778385490372.80/work/Tomcat/localhost/ROOT/
name org.apache.jsp.jsp.hello_jsp
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name hello_jsp
modifier final,public
annotation
interfaces org.apache.jasper.runtime.JspSourceDependent,org.apache.jasper.runtime.JspSourceImports
super-class +-org.apache.jasper.runtime.HttpJspBase
+-javax.servlet.http.HttpServlet
+-javax.servlet.GenericServlet
+-java.lang.Object
class-loader +-org.apache.jasper.servlet.JasperLoader@4bc4fe20





+-org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a
+-sun.misc.Launcher$AppClassLoader@3d4eac69
+-sun.misc.Launcher$ExtClassLoader@f6f4d33
classLoaderHash 4bc4fe20

Affect(row-cnt:1) cost in 7 ms.

查看 ClassLoader 树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[arthas@3880]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@f6f4d33
+-com.taobao.arthas.agent.ArthasClassloader@a3df247
+-sun.misc.Launcher$AppClassLoader@3d4eac69
+-java.net.FactoryURLClassLoader@7b8e788f
+-org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a





+-org.apache.jasper.servlet.JasperLoader@4bc4fe20
Affect(row-cnt:8) cost in 4 ms.

查看 URLClassLoader 实际的 urls

比如上面查看到的 spring LaunchedURLClassLoader 为 org.springframework.boot.loader.LaunchedURLClassLoader ,可以通过 -c <hashcode> 参数来指定 classloader,还有一种方法可以通过使用 --classLoaderClass 指定类名,从而查看 URLClassLoader 实际的 urls:

1
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader

查找 ClassLoader 里的资源文件

查找指定的资源文件: classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r logback-spring.xml

1
2
3
4
[arthas@3880]$ classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r java/lang/String.class
jar:file:/D:/developInstall/JDK/jdk1.8.0_161/jre/lib/rt.jar!/java/lang/String.class

Affect(row-cnt:1) cost in 1 ms.

也可以尝试查找类的 class 文件:

1
2
3
4
[arthas@3880]$ classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r java/lang/String.class
jar:file:/D:/developInstall/JDK/jdk1.8.0_161/jre/lib/rt.jar!/java/lang/String.class

Affect(row-cnt:1) cost in 1 ms.

案例:查找 Top N 线程

查看所有线程信息

1
thread

查看具体线程的栈

查看线程 ID 16 的栈:

1
thread 16

查看 CPU 使用率 top n 线程的栈

参数n 用来指定最忙的前 N 个线程并打印堆栈

1
thread -n 3

参数i 用来指定 cpu 占比统计的采样间隔,单位为毫秒

查看 5 秒内的 CPU 使用率 top n 线程栈

1
thread -n 3 -i 5000

查找线程是否有阻塞

参数b 用来指定找出当前阻塞其他线程的线程

1
thread -b

案例:获取 Spring 应用运行时配置值

众所周之,Spring 应用的配置注入方式非常多。除了我们熟悉的方式,比如

  • System Properties/System Env
  • application.properties/application.yaml
  • spring profiles
  • spring cloud config

还有很多配置注入的方式,令人眼花缭乱。

获取运行时具体配置

对于开发人员来说,在运行时怎样确定某个配置是否生效?它的具体值是什么?

用Arthas可以一行命令获取。

比如获取server.port的具体值:

1
2
$ vmtool --action getInstances --className org.springframework.context.ConfigurableApplicationContext --express 'instances[0].getEnvironment().getProperty("server.port")'
@String[8810]

获取具体的配置来源

但是怎样具体是从哪个配置源来的?

  1. 对于 spring boot应用,可以打开一个新的 terminal 窗口,执行 telnet 127.0.0.1 3658连接上Arthas。

  2. 执行下面的 watch 命令

    1
    org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource findConfigurationProperty
  1. 在原来窗口用上面的vmtool命令来获取server.port的值

可以看到 watch 返回结果里有具体的配置来源application.yml

1
2
3
4
5
6
7
8
$ watch org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource findConfigurationProperty
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 2) cost in 217 ms, listenerId: 5
method=org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource.findConfigurationProperty location=AtExit
ts=2023-11-27 16:08:07.696; [cost=0.327042ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@ConfigurationPropertySourcesPropertySource[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}],
@ConfigurationProperty[[ConfigurationProperty@18fb3ec9 name = server.port, value = 7001, origin = class path resource [application.yml] - 2:9]],

Exit/Stop

reset

Arthas 在 watch/trace 等命令时,实际上是修改了应用的字节码,插入增强的代码。显式执行 reset 命令,可以清除掉这些增强代码。

退出 Arthas

exit 或者 quit 命令可以退出 Arthas。

退出 Arthas 之后,还可以再次用 java -jar arthas-boot.jar 来连接。

彻底退出 Arthas

exit/quit 命令只是退出当前 session,arthas server 还在目标进程中运行。

想完全退出 Arthas,可以执行 stop 命令。

Arthas-boot 支持的参数

arthas-boot.jar 支持很多参数,可以执行 java -jar arthas-boot.jar -h 来查看。

允许外部访问

默认情况下,arthas server 侦听的是 127.0.0.1 这个 IP,如果希望远程可以访问,可以使用--target-ip 的参数。

1
java -jar arthas-boot.jar --target-ip

列出所有的版本

1
java -jar arthas-boot.jar --versions

使用指定版本:

1
java -jar arthas-boot.jar --use-version 3.1.0

只侦听 Telnet 端口,不侦听 HTTP 端口

1
java -jar arthas-boot.jar --telnet-port 9999 --http-port -1

打印运行的详情

1
java -jar arthas-boot.jar -v

简介

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

背景

通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。

开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。

如果您正在考虑在代码中添加一些日志以帮助解决问题,您将必须经历以下阶段:测试、预发,然后生产。这种方法效率低下,更糟糕的是,该问题可能无法解决,因为一旦 JVM 重新启动,它可能无法复现,如上文所述。

Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。

Arthas(阿尔萨斯)能为你做什么?

Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

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

Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

安装启动Arthas

在命令行下面执行(使用和目标进程一致的用户启动,否则可能 attach 失败):

1
2
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

如果下载速度比较慢,可以使用 aliyun 的镜像:java -jar arthas-boot.jar --repo-mirror aliyun --use-http

选择应用 java 进程:

1
2
3
$ $ java -jar arthas-boot.jar
* [1]: 35542
[2]: 71560 math-game.jar

math-game进程是第 2 个,则输入 2,再输入回车/enter。Arthas 会 attach 到目标进程上,并输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[INFO] Try to attach process 71560
[INFO] Attach process 71560 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


wiki: https://arthas.aliyun.com/doc
version: 3.0.5.20181127201536
pid: 71560
time: 2018-11-28 19:16:24

表达式核心变量

无论是匹配表达式也好、观察表达式也罢,他们核心判断变量都是围绕着一个 Arthas 中的通用通知对象 Advice 进行。

它的简略代码结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Advice {

private final ClassLoader loader;
private final Class<?> clazz;
private final ArthasMethod method;
private final Object target;
private final Object[] params;
private final Object returnObj;
private final Throwable throwExp;
private final boolean isBefore;
private final boolean isThrow;
private final boolean isReturn;

// getter/setter
}

这里列一个表格来说明不同变量的含义

变量名 变量解释
loader 本次调用类所在的 ClassLoader
clazz 本次调用类的 Class 引用
method 本次调用方法反射引用
target 本次调用类的实例
params 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组
returnObj 本次调用返回的对象。当且仅当 isReturn==true 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 void,则值为 null
throwExp 本次调用抛出的异常。当且仅当 isThrow==true 成立时有效,表明方法调用是以抛出异常的方式结束。
isBefore 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时 isBefore==true 成立,同时 isThrow==falseisReturn==false,因为在方法刚开始时,还无法确定方法调用将会如何结束。
isThrow 辅助判断标记,当前的方法调用以抛异常的形式结束。
isReturn 辅助判断标记,当前的方法调用以正常返回的形式结束。

所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合 OGNL 脚本语法或者引入了不在表格中的变量,则退出命令的执行;用户可以根据当前的异常信息修正条件表达式观察表达式

命令列表

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

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 - 查看已加载类的方法信息

monitor/watch/trace 相关

注意

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令。

  • monitor - 方法执行监控
  • stack - 输出当前方法被调用的调用路径
  • trace - 方法内部调用路径,并输出方法路径上的每个节点上耗时
  • tt - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
  • watch - 方法执行数据观测

profiler/火焰图

鉴权

options

  • options - 查看或设置 Arthas 全局开关

管道

Arthas 支持使用管道对上述命令的结果进行进一步的处理,如sm java.lang.String * | grep 'index'

  • grep - 搜索满足条件的结果
  • plaintext - 将命令的结果去除 ANSI 颜色
  • wc - 按行统计输出结果

后台异步任务

当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考这里

  • 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天)
  • jobs - 列出所有 job
  • kill - 强制终止任务
  • fg - 将暂停的任务拉到前台执行
  • bg - 将暂停的任务放到后台执行

基础命令

  • base64 - base64 编码转换,和 linux 里的 base64 命令类似
  • cat - 打印文件内容,和 linux 里的 cat 命令类似
  • cls - 清空当前屏幕区域
  • echo - 打印参数,和 linux 里的 echo 命令类似
  • grep - 匹配查找,和 linux 里的 grep 命令类似
  • help - 查看命令帮助信息
  • history - 打印命令历史
  • keymap - Arthas 快捷键列表及自定义快捷键
  • pwd - 返回当前的工作目录,和 linux 命令类似
  • quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
  • reset - 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
  • session - 查看当前会话的信息
  • stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出
  • tee - 复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似
  • version - 输出当前目标 Java 进程所加载的 Arthas 版本号

Arthas 基础教程

Dashboard

dashboard 命令可以查看当前系统的实时数据面板,dashboard 命令可以查看当前系统的实时数据面板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ dashboard
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
-1 C2 CompilerThread0 - -1 - 1.55 0.077 0:8.684 false true
53 Timer-for-arthas-dashboard-07b system 5 RUNNABLE 0.08 0.004 0:0.004 false true
22 scheduling-1 main 5 TIMED_WAI 0.06 0.003 0:0.287 false false
-1 C1 CompilerThread0 - -1 - 0.06 0.003 0:2.171 false true
-1 VM Periodic Task Thread - -1 - 0.03 0.001 0:0.092 false true
49 arthas-NettyHttpTelnetBootstra system 5 RUNNABLE 0.02 0.001 0:0.156 false true
16 Catalina-utility-1 main 1 TIMED_WAI 0.0 0.000 0:0.029 false false
-1 G1 Young RemSet Sampling - -1 - 0.0 0.000 0:0.019 false true
17 Catalina-utility-2 main 1 WAITING 0.0 0.000 0:0.025 false false
34 http-nio-8080-ClientPoller main 5 RUNNABLE 0.0 0.000 0:0.016 false true
23 http-nio-8080-BlockPoller main 5 RUNNABLE 0.0 0.000 0:0.011 false true
-1 VM Thread - -1 - 0.0 0.000 0:0.032 false true
-1 Service Thread - -1 - 0.0 0.000 0:0.006 false true
-1 GC Thread#5 - -1 - 0.0 0.000 0:0.043 false true
Memory used total max usage GC
heap 36M 70M 4096M 0.90% gc.g1_young_generation.count 12
g1_eden_space 6M 18M -1 33.33% 86
g1_old_gen 30M 50M 4096M 0.74% gc.g1_old_generation.count 0
g1_survivor_space 491K 2048K -1 24.01% gc.g1_old_generation.time(ms) 0
nonheap 66M 69M -1 96.56%
codeheap_'non-nmethods' 1M 2M 5M 22.39%
metaspace 46M 47M -1 98.01%
Runtime
os.name Mac OS X
os.version 10.15.4
java.version 15
java.home /Library/Java/JavaVirtualMachines/jdk-15.jdk/Contents/Home
systemload.average 10.68
processors 8
uptime 272s

当运行在 Ali-tomcat 时,会显示当前 tomcat 的实时信息,如 HTTP 请求的 qps, rt, 错误数, 线程池信息等等。

数据说明

  • 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 线程

JVM 内部线程

Java 8 之后支持获取 JVM 内部线程 CPU 时间,这些线程只有名称和 CPU 时间,没有 ID 及状态等信息(显示 ID 为 -1)。 通过内部线程可以观测到 JVM 活动,如 GC、JIT 编译等占用 CPU 情况,方便了解 JVM 整体运行状况。

  • 当 JVM 堆 (heap)/元数据 (metaspace) 空间不足或 OOM 时,可以看到 GC 线程的 CPU 占用率明显高于其他的线程。
  • 当执行trace/watch/tt/redefine 等命令后,可以看到 JIT 线程活动变得更频繁。因为 JVM 热更新 class 字节码时清除了此 class 相关的 JIT 编译结果,需要重新编译。

JVM 内部线程包括下面几种:

  • JIT 编译线程:如 C1 CompilerThread0 , C2 CompilerThread0
  • GC 线程:如GC Thread0 , G1 Young RemSet Sampling
  • 其它内部线程:如VM Periodic Task Thread , VM Thread , Service Thread

参数说明

参数名称 参数说明
[i:] 刷新实时数据的时间间隔 (ms),默认 5000ms
[n:] 刷新实时数据的次数

使用参考:

每隔5000毫秒进行刷新一次,总共刷新3次

1
dashboard -i 5000 -n 3

截图展示


help

提示

[help 指令]的等同于[指令 -help],都是查看具体指令的使用说明。

参数说明

参数名称 参数说明
不接参数 查询当前 arthas 版本支持的指令以及指令描述
[name:] 查询具体指令的使用说明

使用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ help
NAME DESCRIPTION
help Display Arthas Help
auth Authenticates the current session
keymap Display all the available keymap for the specified connection.
sc Search all the classes loaded by JVM
sm Search the method of classes loaded by JVM
classloader Show classloader info
jad Decompile class
getstatic Show the static field of a class
monitor Monitor method execution statistics, e.g. total/success/failure count, average rt, fail rate, etc.
stack Display the stack trace for the specified class and method
thread Display thread info, thread stack
trace Trace the execution time of specified method invocation.
watch Display the input/output parameter, return object, and thrown exception of specified method invocation
tt Time Tunnel
jvm Display the target JVM information
perfcounter Display the perf counter information.
ognl Execute ognl expression.
mc Memory compiler, compiles java files into bytecode and class files in memory.
redefine Redefine classes. @see Instrumentation#redefineClasses(ClassDefinition...)
retransform Retransform classes. @see Instrumentation#retransformClasses(Class...)
dashboard Overview of target jvm's thread, memory, gc, vm, tomcat info.
dump Dump class byte array from JVM
heapdump Heap dump
options View and change various Arthas options
cls Clear the screen
reset Reset all the enhanced classes
version Display Arthas version
session Display current session information
sysprop Display, and change the system properties.
sysenv Display the system env.
vmoption Display, and update the vm diagnostic options.
logger Print logger info, and update the logger level
history Display command history
cat Concatenate and print files
base64 Encode and decode using Base64 representation
echo write arguments to the standard output
pwd Return working directory name
mbean Display the mbean information
grep grep command for pipes.
tee tee command for pipes.
profiler Async Profiler. https://github.com/jvm-profiling-tools/async-profiler
stop Stop/Shutdown Arthas server and exit the console.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ help dashboard
USAGE:
dashboard [-h] [-i <value>] [-n <value>]

SUMMARY:
Overview of target jvm's thread, memory, gc, vm, tomcat info.

EXAMPLES:
dashboard
dashboard -n 10
dashboard -i 2000

WIKI:
https://arthas.aliyun.com/doc/dashboard

OPTIONS:
-h, --help this help
-i, --interval <value> The interval (in ms) between two executions, default is 5000 ms.
-n, --number-of-execution <value> The number of times this command will be executed.

Thread

thread 1 命令会打印线程 ID 1 的栈。

Arthas 支持管道,可以用 thread 1 | grep 'main(' 查找到main class

可以看到main classdemo.MathGame

1
2
$ thread 1 | grep 'main('
at demo.MathGame.main(MathGame.java:17)

提示

查看当前线程信息,查看线程的堆栈。

参数说明

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

使用参考

支持一键展示当前最忙的前 N 个线程并打印堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"Reference Handler" Id=2 cpuUsage=0.0% deltaTime=0ms time=0ms WAITING on java.lang.ref.Reference$Lock@2718d45c
at java.lang.Object.wait(Native Method)
- waiting on java.lang.ref.Reference$Lock@2718d45c
at java.lang.Object.wait(Unknown Source)
at java.lang.ref.Reference.tryHandlePending(Unknown Source)
at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)


"Finalizer" Id=3 cpuUsage=0.0% deltaTime=0ms time=0ms WAITING on java.lang.ref.ReferenceQueue$Lock@6ce75d0d
at java.lang.Object.wait(Native Method)
- waiting on java.lang.ref.ReferenceQueue$Lock@6ce75d0d
at java.lang.ref.ReferenceQueue.remove(Unknown Source)
at java.lang.ref.ReferenceQueue.remove(Unknown Source)
at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)


"Signal Dispatcher" Id=4 cpuUsage=0.0% deltaTime=0ms time=0ms RUNNABLE
  • 没有线程 ID,包含[Internal]表示为 JVM 内部线程,参考dashboard命令的介绍。
  • cpuUsage为采样间隔时间内线程的 CPU 使用率,与dashboard命令的数据一致。
  • deltaTime为采样间隔时间内线程的增量 CPU 时间,小于 1ms 时被取整显示为 0ms。
  • time 线程运行总 CPU 时间。

注意:线程栈为第二采样结束时获取,不能表明采样间隔时间内该线程都是在处理相同的任务。建议间隔时间不要太长,可能间隔时间越大越不准确。 可以根据具体情况尝试指定不同的间隔时间,观察输出结果。

当没有参数时,显示第一页线程的信息

默认按照 CPU 增量时间降序排列,只显示第一页数据。

thread –all, 显示所有匹配的线程

显示所有匹配线程信息,有时需要获取全部 JVM 的线程数据进行分析。

thread id, 显示指定线程的运行堆栈

1
2
3
4
5
6
$ thread 1
"main" Id=1 TIMED_WAITING
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Unknown Source)
at java.util.concurrent.TimeUnit.sleep(Unknown Source)
at demo.MathGame.main(MathGame.java:17)

thread -b, 找出当前阻塞其他线程的线程

有时候我们发现应用卡住了, 通常是由于某个线程拿住了某个锁, 并且其他线程都在等待这把锁造成的。 为了排查这类问题, arthas 提供了thread -b, 一键找出那个罪魁祸首。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ thread -b
"http-bio-8080-exec-4" Id=27 TIMED_WAITING
at java.lang.Thread.sleep(Native Method)
at test.arthas.TestThreadBlocking.doGet(TestThreadBlocking.java:22)
- locked java.lang.Object@725be470 <---- but blocks 4 other threads!
at javax.servlet.http.HttpServlet.service(HttpServlet.java:624)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at test.filter.TestDurexFilter.doFilter(TestDurexFilter.java:46)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
at com.taobao.tomcat.valves.ContextLoadFilterValve$FilterChainAdapter.doFilter(ContextLoadFilterValve.java:191)
at com.taobao.eagleeye.EagleEyeFilter.doFilter(EagleEyeFilter.java:81)
at com.taobao.tomcat.valves.ContextLoadFilterValve.invoke(ContextLoadFilterValve.java:150)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:429)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1085)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:625)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
- locked org.apache.tomcat.util.net.SocketWrapper@7127ee12
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

Number of locked synchronizers = 1
- java.util.concurrent.ThreadPoolExecutor$Worker@31a6493e

warn no-icon

注意

注意, 目前只支持找出 synchronized 关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持。

thread -i, 指定采样时间间隔

  • thread -i 1000 : 统计最近 1000ms 内的线程 CPU 时间。
  • thread -n 3 -i 1000 : 列出 1000ms 内最忙的 3 个线程栈

thread –state ,查看指定状态的线程

thread –state ,查看指定状态的线程

1
2
3
4
5
6
[arthas@28114]$ thread --state WAITING
Threads Total: 16, NEW: 0, RUNNABLE: 9, BLOCKED: 0, WAITING: 3, TIMED_WAITING: 4, TERMINATED: 0
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
3 Finalizer system 8 WAITING 0.0 0.000 0:0.000 false true
20 arthas-UserStat system 9 WAITING 0.0 0.000 0:0.001 false true
14 arthas-timer system 9 WAITING 0.0 0.000 0:0.000 false true

Sc

可以通过 sc 命令 来查找 JVM 里已加载的类:

“Search-Class” 的简写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息,这个命令支持的参数有 [d][E][f][x:]

参数说明

参数名称 参数说明
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() 返回值。长格式[classLoaderStr <arg>]

提示

class-pattern 支持全限定名,如 com.taobao.test.AAA,也支持 com/taobao/test/AAA 这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把/替换为.啦。

提示

sc 默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true开关

使用参考

  • 模糊搜索

    1
    2
    3
    $ sc demo.*
    demo.MathGame
    Affect(row-cnt:1) cost in 55 ms.
  • 打印类的详细信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    $ sc -d demo.MathGame
    class-info demo.MathGame
    code-source /private/tmp/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 +-sun.misc.Launcher$AppClassLoader@3d4eac69
    +-sun.misc.Launcher$ExtClassLoader@66350f69
    classLoaderHash 3d4eac69

    Affect(row-cnt:1) cost in 875 ms.
  • 打印出类的 Field 信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    $ sc -d -f demo.MathGame
    class-info demo.MathGame
    code-source /private/tmp/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 +-sun.misc.Launcher$AppClassLoader@3d4eac69
    +-sun.misc.Launcher$ExtClassLoader@66350f69
    classLoaderHash 3d4eac69
    fields modifierprivate,static
    type java.util.Random
    name random
    value java.util.Random@522b4
    08a

    modifierprivate
    type int
    name illegalArgumentCount


    Affect(row-cnt:1) cost in 19 ms.
  • 通过 ClassLoader#toString 查找类(前提:有一个 toString()返回值是apo的类加载器,加载的类中包含demo.MathGame, demo.MyBar, demo.MyFoo3 个类)

    1
    2
    3
    4
    5
    $ sc -cs apo *demo*
    demo.MathGame
    demo.MyBar
    demo.MyFoo
    Affect(row-cnt:3) cost in 56 ms.

Jad

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

提示

反编译指定已加载类的源码

jad 命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;如需批量下载指定包的目录的 class 字节码可以参考 dump

  • 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
  • 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解

参数说明

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

使用参考

反编译java.lang.String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ jad java.lang.String

ClassLoader:

Location:


/*
* Decompiled with CFR.
*/
package java.lang;

import java.io.ObjectStreamField;
import java.io.Serializable;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
...
public String(byte[] byArray, int n, int n2, Charset charset) {
/*460*/ if (charset == null) {
throw new NullPointerException("charset");
}
/*462*/ String.checkBounds(byArray, n, n2);
/*463*/ this.value = StringCoding.decode(charset, byArray, n, n2);
}
...

反编译时只显示源代码

默认情况下,反编译结果里会带有ClassLoader信息,通过--source-only选项,可以只打印源代码。方便和mc/retransform命令结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ jad --source-only demo.MathGame
/*
* Decompiled with CFR 0_132.
*/
package demo;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
private static Random random = new Random();
public int illegalArgumentCount = 0;
...

反编译指定的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ jad demo.MathGame main

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@232204a1
+-sun.misc.Launcher$ExtClassLoader@7f31245a

Location:
/private/tmp/math-game.jar

public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
/*16*/ game.run();
/*17*/ TimeUnit.SECONDS.sleep(1L);
}
}

反编译时不显示行号

--lineNumber 参数默认值为 true,显示指定为 false 则不打印行号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ jad demo.MathGame main --lineNumber false

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@232204a1
+-sun.misc.Launcher$ExtClassLoader@7f31245a

Location:
/private/tmp/math-game.jar

public static void main(String[] args) throws InterruptedException {
MathGame game = new MathGame();
while (true) {
game.run();
TimeUnit.SECONDS.sleep(1L);
}
}

反编译时指定 ClassLoader

提示

当有多个 ClassLoader 都加载了这个类时,jad 命令会输出对应 ClassLoader 实例的 hashcode,然后你只需要重新执行 jad 命令,并使用参数 -c <hashcode> 就可以反编译指定 ClassLoader 加载的那个类了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ jad org.apache.log4j.Logger

Found more than one class for: org.apache.log4j.Logger, Please use jad -c hashcode org.apache.log4j.Logger
HASHCODE CLASSLOADER
69dcaba4 +-monitor's ModuleClassLoader
6e51ad67 +-java.net.URLClassLoader@6e51ad67
+-sun.misc.Launcher$AppClassLoader@6951a712
+-sun.misc.Launcher$ExtClassLoader@6fafc4c2
2bdd9114 +-pandora-qos-service's ModuleClassLoader
4c0df5f8 +-pandora-framework's ModuleClassLoader

Affect(row-cnt:0) cost in 38 ms.
$ jad org.apache.log4j.Logger -c 69dcaba4

ClassLoader:
+-monitor's ModuleClassLoader

Location:
/Users/admin/app/log4j-1.2.14.jar

package org.apache.log4j;

import org.apache.log4j.spi.*;

public class Logger extends Category
{
private static final String FQCN;

protected Logger(String name)
{
super(name);
}

...

Affect(row-cnt:1) cost in 190 ms.

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

--classLoaderClass 的值是 ClassLoader 的类名,只有匹配到唯一的 ClassLoader 实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。


Watch

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

提示

函数执行数据观测

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

参数说明

watch 的参数比较多,主要是因为它能在 4 个不同的场景观察对象

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

这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。

观察的维度也比较多,主要体现在参数 advice 的数据结构上。Advice 参数最主要是封装了通知节点的所有信息。

特别说明

  • watch 命令定义了 4 个观察事件点,即 -b 函数调用前,-e 函数异常后,-s 函数返回后,-f 函数结束后
  • 4 个观察事件点 -b-e-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
  • 这里要注意函数入参函数出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表函数入参外,其余事件都代表函数出参
  • 当使用 -b 时,由于观察事件点是在函数调用前,此时返回值或异常均不存在
  • 在 watch 命令的结果里,会打印出location信息。location有三种可能值:AtEnterAtExitAtExceptionExit。对应函数入口,函数正常 return,函数抛出异常。

使用参考

观察函数调用返回时的参数、this 对象和返回值

提示

观察表达式,默认值是 {params, target, returnObj}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ watch demo.MathGame primeFactors -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 32 ms, listenerId: 5
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2021-08-31 15:22:57; [cost=0.220625ms] result=@ArrayList[
@Object[][
@Integer[-179173],
],
@MathGame[
random=@Random[java.util.Random@31cefde0],
illegalArgumentCount=@Integer[44],
],
null,
]
method=demo.MathGame.primeFactors location=AtExit
ts=2021-08-31 15:22:58; [cost=1.020982ms] result=@ArrayList[
@Object[][
@Integer[1],
],
@MathGame[
random=@Random[java.util.Random@31cefde0],
illegalArgumentCount=@Integer[44],
],
@ArrayList[
@Integer[2],
@Integer[2],
@Integer[26947],
],
]
  • 上面的结果里,说明函数被执行了两次,第一次结果是location=AtExceptionExit,说明函数抛出异常了,因此returnObj是 null
  • 在第二次结果里是location=AtExit,说明函数正常返回,因此可以看到returnObj结果是一个 ArrayList

指定 Class最大匹配数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ watch demo.MathGame primeFactors -m 1
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 302 ms, listenerId: 3
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2022-12-25 19:58:41; [cost=0.222419ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@MathGame[demo.MathGame@3bf400],
null,
]
method=demo.MathGame.primeFactors location=AtExceptionExit
ts=2022-12-25 19:58:51; [cost=0.046928ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@MathGame[demo.MathGame@3bf400],
null,
]

观察函数调用入口的参数和返回值

1
2
3
4
5
6
7
8
9
$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2 -b
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 50 ms.
ts=2018-12-03 19:23:23; [cost=0.0353ms] result=@ArrayList[
@Object[][
@Integer[-1077465243],
],
null,
]
  • 对比前一个例子,返回值为空(事件点为函数执行前,因此获取不到返回值)

Vmtool

通过 vmtool 命令,可以搜索内存对象。

获取 class 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
$ vmtool --action getInstances --className java.lang.String --limit 10
@String[][
@String[com/taobao/arthas/core/shell/session/Session],
@String[com.taobao.arthas.core.shell.session.Session],
@String[com/taobao/arthas/core/shell/session/Session],
@String[com/taobao/arthas/core/shell/session/Session],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/],
@String[java/util/concurrent/ConcurrentHashMap$ValueIterator],
@String[java/util/concurrent/locks/LockSupport],
]

提示

通过 –limit 参数,可以限制返回值数量,避免获取超大数据时对 JVM 造成压力。默认值是 10。

调用实例方法

1
2
3
4
[arthas@13416]$ vmtool --action  getInstances  --className demo.MathGame --express 'instances[0].primeFactors(3)' -x 3
@ArrayList[
@Integer[3],
]

退出 Arthas

exit 或者 quit 命令可以退出 Arthas。

退出 Arthas 之后,还可以再次用 java -jar arthas-boot.jar 来连接。

彻底退出 Arthas

exit/quit 命令只是退出当前 session,arthas server 还在目标进程中运行。

想完全退出 Arthas,可以执行 stop 命令。

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

现实世界的例子

假设您的存储卡上有一些照片,并且需要将它们传输到计算机上。要传输它们,您需要某种与计算机端口兼容的适配器,以便可以将存储卡连接到计算机。在这种情况下,读卡器是一个适配器。
另一个例子是著名的电源适配器;三脚插头不能连接两脚插座,需要使用兼容两脚插座的电源适配器。
另一个例子是翻译人员将一个人所说的单词翻译成另一个人所说的单词

用简单的话来说

适配器模式允许您将不兼容的对象包装在适配器中,以使其与另一个类兼容。

问题

假如你正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。

在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。

你可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 你可能根本没有程序库的源代码, 从而无法对其进行修改。

解决方案

你可以创建一个适配器。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。

适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 例如, 你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。

适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下:

  1. 适配器实现与其中一个现有对象兼容的接口。
  2. 现有对象可以使用该接口安全地调用适配器方法。
  3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

有时你甚至可以创建一个双向适配器来实现双向转换调用。

让我们回到股票市场程序。 为了解决数据格式不兼容的问题, 你可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。

适配器模式结构

实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。

程序示例:

对象适配器模式,是通过组合的形式来完成适配,在实际开发中,如果能够适用组合的形式,就尽量不用要继承。

1
2
3
4
5
6
7
package com.design.patterns.adapter;

public class Adaptee {
public void adapteeRequest() {
System.out.println("被适配者的方法");
}
}
1
2
3
4
5
package com.design.patterns.adapter;

public interface Target {
void request();
}
1
2
3
4
5
6
7
8
package com.design.patterns.adapter;

public class ConcreteTarget implements Target {
@Override
public void request() {
System.out.println("concreteTarget目标方法");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.design.patterns.adapter;

public class Adapter implements Target {
private Adaptee adaptee = new Adaptee();

@Override
public void request() {
// ...
adaptee.adapteeRequest();
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.design.patterns.adapter;

public class Test {
public static void main(String[] args) {
Target target = new ConcreteTarget();
target.request();

Target adapterTarget = new Adapter();
adapterTarget.request();
}
}

优点

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  • 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
  • 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点

  • 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构
  • 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

已知使用:

  • java.util.Arrays#asList()
  • java.util.Collections#list()
  • java.util.Collections#enumeration()
  • javax.xml.bind.annotation.adapters.XMLAdapter

模式动机

在策略模式中定义了一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式。策略模式是一种对象行为型模式。

在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。

在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。

除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。

为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。

真实世界类比

假如你需要前往机场。 你可以选择乘坐公共汽车、 预约出租车或骑自行车。 这些就是你的出行策略。 你可以根据预算或时间等因素来选择其中一种策略。

策略模式结构

  1. 上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。
  2. 策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。
  3. 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。
  4. 当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。
  5. 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

现实中的例子

某商场在618期间推出多种促销活动,在618活动前期使用立减促销的策略,在618活动中期采用满减促销的策略,在618活动后期采用返现促销的策略。

程序化示例

我们先介绍一下促销策略接口及其实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface Strategy {

// 促销
void doPromotion();

}

public class FanXianStrategy implements Strategy {

@Override
public void doPromotion() {
System.out.println("返现促销!");
}

}

public class LiJianStrategy implements Strategy {

@Override
public void doPromotion() {
System.out.println("立减促销!");
}

}

public class ManJianStrategy implements Strategy {

@Override
public void doPromotion() {
System.out.println("满减促销!");
}

}

这是强大的活动类,他可以根据618时间节点选择促销策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Activity {

private Strategy strategy;

public Activity(Strategy strategy) {
this.strategy = strategy;
}

public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy() {
strategy.doPromotion();
}

}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {

public static void main(String[] args) {

LiJianStrategy liJianStrategy = new LiJianStrategy();
Activity activity618 = new Activity(liJianStrategy);
activity618.executeStrategy();

ManJianStrategy manJianStrategy = new ManJianStrategy();
activity618.setStrategy(manJianStrategy);
activity618.executeStrategy();

FanXianStrategy fanXianStrategy = new FanXianStrategy();
activity618.setStrategy(fanXianStrategy);
activity618.executeStrategy();

}

}

程序输出:

1
2
3
立减促销!
满减促销!
返现促销!

UML:

策略模式适合应用场景

  • 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。(策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。)
  • 当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。(策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。)
  • 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与其算法实现细节隔离开来。(策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。)
  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。(策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。)

模式分析

  • 策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
  • 在策略模式中,应当由客户端自己决定在什么情况下使用什么具体策略角色。
  • 策略模式仅仅封装算法,提供新算法插入到已有系统中,以及老算法从系统中“退休”的方便,策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定。这在一定程度上提高了系统的灵活性,但是客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,这也是策略模式的缺点之一,在一定程度上增加了客户端的使用难度。

优点

  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
  • 策略模式提供了管理相关的算法族的办法。
  • 策略模式提供了可以替换继承关系的办法。
  • 使用策略模式可以避免使用多重条件转移语句。

缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

总结

  • 在策略模式中定义了一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式。策略模式是一种对象行为型模式。
  • 策略模式包含三个角色:环境类在解决某个问题时可以采用多种策略,在环境类中维护一个对抽象策略类的引用实例;抽象策略类为所支持的算法声明了抽象方法,是所有策略类的父类;具体策略类实现了在抽象策略类中定义的算法。
  • 策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。
  • 策略模式主要优点在于对“开闭原则”的完美支持,在不修改原有系统的基础上可以更换算法或者增加新的算法,它很好地管理算法族,提高了代码的复用性,是一种替换继承,避免多重条件转移语句的实现方式;其缺点在于客户端必须知道所有的策略类,并理解其区别,同时在一定程度上增加了系统中类的个数,可能会存在很多策略类。
  • 策略模式适用情况包括:在一个系统里面有许多类,它们之间的区别仅在于它们的行为,使用策略模式可以动态地让一个对象在许多行为中选择一种行为;一个系统需要动态地在几种算法中选择一种;避免使用难以维护的多重条件选择语句;希望在具体策略类中封装算法和与相关的数据结构。

代理模式: 为一个对象提供一个替身,以控制对目标对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,及扩展目标对象的功能。

被代理的对象可以是远程对象,创建开销大的对象或需要安全控制的对象。

在现实生活中,一个对象不能直接访问另一个对象,这时需要找中介来访问目标对象,此时的中介就是代理对象。例如:租房子时,我们无法与房东取得联系,只能通过某网站与中介进行交易,获取自己心仪的房间等等。

代理模式结构

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。代理必须遵循该接口才能伪装成服务对象。
  • 真实主题(Real Subject)类:实现了出现主题中的具体业务,是代理对象所代理的真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
  • 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。

现实中的例子

我们模拟在现实生活通过携程火车票系统购票的过程,假设我们通过代理购票系统购票时,购票系统首先会校验购票人的购票资格,校验成功则会调用真正的购票系统进行购票,购票成功后会收取购票人的一定的服务费。

程序化示例

1、抽象主题类:代理类与被代理类都需要实现的接口。

1
2
3
4
5
public interface Ticketing {

void buyTicket();

}

2、真实主题类: 目标类

1
2
3
4
5
6
public class RailwaySite implements Ticketing {
@Override
public void buyTicket() {
System.out.println("调用官方系统购票成功!");
}
}

3、代理类:需要实现被代理类的接口,使用代理方法调用目标对象的方法,同时实现对目标方法的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ProxyTicketSystem implements Ticketing {

private Ticketing ticket;

public ProxyTicketSystem(Ticketing ticket) {
this.ticket = ticket;
}

@Override
public void buyTicket() {
preHandle();
ticket.buyTicket();
postHandle();
}

private void preHandle() {
System.out.println("代理(携程火车票)系统校验购票人资格!");
System.out.println("校验成功!");
}

private void postHandle() {
System.out.println("代理(携程火车票)系统收取购票人服务费10元!");
}

}

4、客户端:需要创建被代理对象和代理对象,并进行组合调用。

1
2
3
4
5
6
7
8
9
public class Client {

public static void main(String[] args) {
RailwaySite railwaySite = new RailwaySite();
ProxyTicketSystem proxy = new ProxyTicketSystem(railwaySite);
proxy.buyTicket();
}

}

程序输出:

1
2
3
4
代理(携程火车票)系统校验购票人资格!
校验成功!
调用官方系统购票成功!
代理(携程火车票)系统收取购票人服务费10元!

适用性

当需要比简单指针更通用或更复杂的对象引用时,就需要使用代理模式。以下是代理模式适用的几种常见情况。

  • 远程代理为不同地址空间的对象提供本地代表。
  • 虚拟代理按需创建昂贵的对象。
  • 保护代理控制对原始对象的访问。当对象应具有不同访问权限时,保护代理非常有用。

已知使用

  • java.lang.reflect.Proxyopen in new window
  • Apache Commons Proxy

观察者模式,也称为监听者、发布-订阅。

意图

定义对象之间的一对多依赖关系,以便当一个对象更改状态时,其所有依赖对象都会收到通知并自动更新。

问题

假如你有两种类型的对象: 顾客商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者(publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。

现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

观察者模式结构

现实世界的例子

在遥远的土地上居住着农夫们和动物们。他们大部分时间都在户外,所以他们密切关注天气变化。当天气晴朗时,农夫们会出去干活,动物们会出去觅食;当天空开始下雨时,农夫们会回家收衣服,动物们会找地方避雨;当天气开始起风了,农夫们会在家打麻将,动物们会睡大觉;当天气变冷时,农夫们都在家烤火,动物们会抱团取暖。可以说,他们一直在观察天气。

程序化示例

我们首先介绍一下WeatherObserver接口和我们具体的观察者,Farmer以及Animal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public interface WeatherObserver {

void update(WeatherType weatherType);

}


public class Farmer implements WeatherObserver {

@Override
public void update(WeatherType weatherType) {
switch (weatherType) {
case SUNNY:
System.out.println("天气晴朗,农夫们出门干活了!");
break;
case RAINY:
System.out.println("下雨了,农夫们回家收衣服了!");
break;
case WINDY:
System.out.println("起风了,农夫们在家打麻将!");
break;
case COLD:
System.out.println("天冷了,农夫们在家烤火!");
}
}

}

public class Animal implements WeatherObserver {

@Override
public void update(WeatherType weatherType) {

switch (weatherType) {
case SUNNY:
System.out.println("天气晴朗,动物们出去觅食了!");
break;
case RAINY:
System.out.println("下雨了,动物们找地方避雨了!");
break;
case WINDY:
System.out.println("起风了,动物们在睡大觉!");
break;
case COLD:
System.out.println("天冷了,动物们抱团取暖!");
}
}

}

然后是我们被观察的主体天气Weather类,并且提供了不断变化的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Weather {

private WeatherType currentWeather;

private List<WeatherObserver> observers;

public void addObserver(WeatherObserver observer) {
observers.add(observer);
}

public void removeObserver(WeatherObserver observer) {
observers.remove(observer);
}

public Weather() {
this.observers = new ArrayList<>();
this.currentWeather = WeatherType.COLD;
}

private void notifyObservers() {
for (WeatherObserver observer : observers) {
observer.update(currentWeather);
}
}

public WeatherType getCurrentWeather() {
return currentWeather;
}

public void setCurrentWeather(WeatherType currentWeather) {
WeatherType preWeather = this.currentWeather;
this.currentWeather = currentWeather;
if (preWeather != currentWeather) {
notifyObservers();
}
}

public void timePasses() {
WeatherType[] weatherEnums = WeatherType.values();
WeatherType nextWeather = weatherEnums[(currentWeather.ordinal() + 1) % weatherEnums.length];
System.out.println("CurrentWeather:" + nextWeather);
this.setCurrentWeather(nextWeather);
}

}

我们定义了天气的枚举类,其中定了4种天气。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum WeatherType {

SUNNY("Sunny"),
RAINY("Rainy"),
WINDY("Windy"),
COLD("Cold");

private final String description;

public String getDescription() {
return description;
}

WeatherType(String description) {
this.description = description;
}

@Override
public String toString() {
return this.name().toLowerCase();
}

}

然后是完整的实际示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {

Weather weather = new Weather();
Farmer farmer = new Farmer();
Animal animal = new Animal();
weather.addObserver(farmer);
weather.addObserver(animal);

weather.timePasses();
weather.timePasses();
weather.timePasses();
weather.timePasses();

}

}

程序输出:

1
2
3
4
5
6
7
8
9
10
11
12
CurrentWeather:sunny
天气晴朗,农夫们出门干活了!
天气晴朗,动物们出去觅食了!
CurrentWeather:rainy
下雨了,农夫们回家收衣服了!
下雨了,动物们找地方避雨了!
CurrentWeather:windy
起风了,农夫们在家打麻将!
起风了,动物们在睡大觉!
CurrentWeather:cold
天冷了,农夫们在家烤火!
天冷了,动物们抱团取暖!

适用性

在以下任一情况下使用观察者模式:

  • 当对一个对象的更改需要更改其他对象时,您不知道需要更改多少个对象。
  • 当一个对象应该能够通知其他对象而无需知道这些对象是谁时。换句话说,您不希望这些对象紧密耦合。

已知使用:

  • java.util.Observeropen in new window
  • java.util.EventListeneropen in new window
  • javax.servlet.http.HttpSessionBindingListeneropen in new window
  • RxJavaopen in new window

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

使用建造者模式,用户就只需要指定需要建造的类型,具体的建造过程和细节并不需要知道。

建造者模式允许修改一个产品的内部表示。

它将构造和表示两块代码隔离开来。

它很好的控制了构建过程。

建造者模式流程说明:

  1. 创建大型产品建造者
  2. 客户端创建指挥者 Director 对象并配置它所需要的建造者 Builder 对象
  3. Director 负责通知 builder 何时建造 product 的部件
  4. Builder 处理 director 的请求并添加 product 的部件
  5. 客户端从 builder 处获得 product

问题

假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。

例如, 我们来思考如何创建一个房屋House对象。 建造一栋简单的房屋, 首先你需要建造四面墙和地板, 安装房门和一套窗户, 然后再建造一个屋顶。 但是如果你想要一栋更宽敞更明亮的房屋, 还要有院子和其他设施 (例如暖气、 排水和供电设备), 那又该怎么办呢?

最简单的方法是扩展房屋基类, 然后创建一系列涵盖所有参数组合的子类。 但最终你将面对相当数量的子类。 任何新增的参数 (例如门廊类型) 都会让这个层次结构更加复杂。

另一种方法则无需生成子类。 你可以在房屋基类中创建一个包括所有可能参数的超级构造函数, 并用它来控制房屋对象。 这种方法确实可以避免生成子类, 但它却会造成另外一个问题。

通常情况下, 绝大部分的参数都没有使用, 这使得对于构造函数的调用十分不简洁。 例如, 只有很少的房子有游泳池, 因此与游泳池相关的参数十之八九是毫无用处的。

解决方案

生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

该模式会将对象构造过程划分为一组步骤, 比如 build­Walls创建墙壁和 build­Door创建房门创建房门等。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。

在这种情况下, 你可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。

例如, 假设第一个建造者使用木头和玻璃制造房屋, 第二个建造者使用石头和钢铁, 而第三个建造者使用黄金和钻石。 在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡, 而第三个则会给你一座宫殿。 但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。

指挥者

你可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的指挥类。 指挥类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现。

严格来说, 你的程序中并不一定需要指挥类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 指挥类中非常适合放入各种例行构造流程, 以便在程序中反复使用。

此外, 对于客户端代码来说, 指挥类完全隐藏了产品构造细节。 客户端只需要将一个生成器与主管类关联, 然后使用主管类来构造产品, 就能从生成器处获得构造结果了。

生成器模式结构

  1. 建造者 (Builder) 接口声明在所有类型建造者中通用的产品构造步骤。
  2. 具体建造者 (Concrete Builders) 提供构造过程的不同实现。 具体建造者也可以构造不遵循通用接口的产品。
  3. 产品 (Products) 是最终生成的对象。 由不同建造者构造的产品无需属于同一类层次结构或接口。
  4. 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  5. 客户端 (Client) 必须将某个建造者对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用建造者对象完成后续所有的构造任务。 但在客户端将建造者对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的建造者。

代码示例:

下面关于生成器模式的例子演示了你可以如何复用相同的对象构造代码来生成不同类型的产品——例如汽车 (Car)。

汽车是一个复杂对象, 有数百种不同的制造方法。 我们没有在 汽车类中塞入一个巨型构造函数, 而是将汽车组装代码抽取到单独的汽车生成器类中。 该类中有一组方法可用来配置汽车的各种部件。

如果客户端代码需要组装一辆与众不同、 精心调教的汽车, 它可以直接调用生成器。 或者, 客户端可以将组装工作委托给主管类, 因为主管类知道如何使用生成器制造最受欢迎的几种型号汽车。

比如说我是个老司机,但是除了开车还想造车,但是车的构造实在是太复杂了,那么我们就可以将车拆分…

4个轮子、1个底盘、1个发动机,1个驾驶位

好了,为了简便,就造这四个吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Car {

private CarType carType;

private WheelType wheels;

private ChassisType chassis;

private EngineType engine;

private int seats;

public Car() {
}

public Car(CarType carType, WheelType wheels, ChassisType chassis, EngineType engine, int seat) {
this.carType = carType;
this.wheels = wheels;
this.chassis = chassis;
this.engine = engine;
this.seats = seat;
}

public CarType getCarType() {
return carType;
}

public WheelType getWheels() {
return wheels;
}

public ChassisType getChassis() {
return chassis;
}

public EngineType getEngine() {
return engine;
}

public int getSeat() {
return seats;
}

@Override
public String toString() {
return "Car{" +
"carType=" + carType +
", wheels=" + wheels +
", chassis=" + chassis +
", engine=" + engine +
", seats=" + seats +
'}';
}
}

我们有汽车零部件的各种枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public enum CarType {
CITY_CAR,
SPORT_CAR,
SUV
}

public enum WheelType {
CITY_CAR_WHEEL,
SPORT_CAR_WHEEL,
SUV_CAR_WHEEL
}

public enum ChassisType {
CITY_CAR_CHASSIS,
SPORT_CAR_CHASSIS,
SUV_CAR_CHASSIS
}

public enum EngineType {
CITY_CAR_ENGINE,
SPORT_CAR_ENGINE,
SUV_CAR_ENGINE
}

然后我需要一些具体的汽车构造者,这里我们有三种具体的汽车构建者:城市轿车构建者CityCarBuilder、运动跑车构建者SportCarBuilder、SUV汽车构建者SUVCarBuilder

CityCarBuilder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.design.patterns.builder;

/**
* @author wotzc
**/
public class CityCarBuilder implements Builder {

private CarType carType;
private WheelType wheels;
private ChassisType chassis;
private EngineType engine;
private int seat;


@Override
public void createCar() {
this.carType = CarType.CITY_CAR;
}

@Override
public void buildWheels() {
this.wheels = WheelType.CITY_CAR_WHEEL;
}

@Override
public void buildChassis() {
this.chassis = ChassisType.CITY_CAR_CHASSIS;
}

@Override
public void buildEngine() {
this.engine = EngineType.CITY_CAR_ENGINE;
}

@Override
public void buildSeat() {
this.seat = 5;
}

@Override
public Car getCar() {
return new Car(carType, wheels, chassis, engine, seat);
}

}

SportCarBuilder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.design.patterns.builder;

/**
* @author wotzc
**/
public class SportCarBuilder implements Builder {

private CarType carType;
private WheelType wheels;
private ChassisType chassis;
private EngineType engine;
private int seat;

@Override
public void createCar() {
this.carType = CarType.SPORT_CAR;
}

@Override
public void buildWheels() {
this.wheels = WheelType.SPORT_CAR_WHEEL;
}

@Override
public void buildChassis() {
this.chassis = ChassisType.SPORT_CAR_CHASSIS;
}

@Override
public void buildEngine() {
this.engine = EngineType.SPORT_CAR_ENGINE;
}

@Override
public void buildSeat() {
this.seat = 2;
}

@Override
public Car getCar() {
return new Car(carType, wheels, chassis, engine, seat);
}
}

SUVCarBuilder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.design.patterns.builder;

/**
* @author wotzc
**/
public class SUVCarBuilder implements Builder {

private CarType carType;
private WheelType wheels;
private ChassisType chassis;
private EngineType engine;
private int seat;

@Override
public void createCar() {
this.carType = CarType.SUV;
}

@Override
public void buildWheels() {
this.wheels = WheelType.SUV_CAR_WHEEL;
}

@Override
public void buildChassis() {
this.chassis = ChassisType.SUV_CAR_CHASSIS;
}

@Override
public void buildEngine() {
this.engine = EngineType.SUV_CAR_ENGINE;
}

@Override
public void buildSeat() {
this.seat = 7;
}

@Override
public Car getCar() {
return new Car(carType, wheels, chassis, engine, seat);
}
}

这些建造者都实现了Builder接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.design.patterns.builder;

/**
* @author wotzc
**/
public interface Builder {

// 创建一个汽车对象
void createCar();

// 建造汽车车轮
void buildWheels();

// 建造汽车地盘
void buildChassis();

// 建造发动机
void buildEngine();

// 建造驾驶位
void buildSeat();

Car getCar();

}

最后我们需要一个Director类负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
会很有帮助。由于客户端可以直接控制生成器,我们可以调用Director类的setBuilder来切换产品的具体建造者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.design.patterns.builder;

/**
* @author wotzc
**/
public class Director {

private Builder builder;

public Car constructCar() {
builder.createCar();
builder.buildWheels();
builder.buildChassis();
builder.buildEngine();
builder.buildSeat();
return builder.getCar();
}

public Director(Builder builder) {
this.builder = builder;
}

public void setBuilder(Builder builder) {
this.builder = builder;
}

public Builder getBuilder() {
return builder;
}
}

现在指挥者和建造者都已经准备好了,可以进行建造了,调用指挥者的constructCar() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.design.patterns.builder;

/**
* @author wotzc
**/
public class Main {

public static void main(String[] args) {
// 生产城市汽车
CityCarBuilder cityCarBuilder = new CityCarBuilder();
Director director = new Director(cityCarBuilder);
Car cityCar = director.constructCar();
System.out.println(cityCar.toString());

// 生产运动汽车
SportCarBuilder sportCarBuilder = new SportCarBuilder();
director.setBuilder(sportCarBuilder);
Car sportCar = director.constructCar();
System.out.println(sportCar.toString());

// 生产SUV
SUVCarBuilder suvCarBuilder = new SUVCarBuilder();
director.setBuilder(suvCarBuilder);
Car suv = director.constructCar();
System.out.println(suv.toString());

}

}

程序输出:

1
2
3
Car{carType=CITY_CAR, wheels=CITY_CAR_WHEEL, chassis=CITY_CAR_CHASSIS, engine=CITY_CAR_ENGINE, seats=5}
Car{carType=SPORT_CAR, wheels=SPORT_CAR_WHEEL, chassis=SPORT_CAR_CHASSIS, engine=SPORT_CAR_ENGINE, seats=2}
Car{carType=SUV, wheels=SUV_CAR_WHEEL, chassis=SUV_CAR_CHASSIS, engine=SUV_CAR_ENGINE, seats=7}

生成器模式适合应用场景:

使用生成器模式可避免 “重叠构造函数 (telescoping constructor)” 的出现。

假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。

1
2
3
4
5
class Pizza {
Pizza(int size) { …… }
Pizza(int size, boolean cheese) { …… }
Pizza(int size, boolean cheese, boolean pepperoni) { …… }
// ……

只有在 C# 或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。

生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。

当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。

如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。

基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。

已知使用:

  • java.lang.StringBuilder
  • java.nio.ByteBuffer as well as similar buffers such as FloatBuffer, IntBuffer and so on.
  • java.lang.StringBuffer
  • All implementations of java.lang.Appendable
  • Apache Camel builders
  • Apache Commons Option.Builder

原型模式的定义如下:

Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.

使用原型实例指定要创建的对象类型,并通过复制此原型来创建新对象。

还记得多莉吗?克隆羊!我们不讨论细节,但这里的关键点是这一切都与克隆有关。

用简单的话来说

通过克隆基于现有对象创建对象。

维基百科说

原型模式是软件开发中的一种创造性设计模式。当要创建的对象的类型由原型实例确定时使用,原型实例将被克隆以生成新的对象。

简而言之,它允许你创建一个现有对象的副本,并根据你的需要对其进行修改,而不是费尽周折地从头开始创建一个对象并进行设置。

原型模式的结构很简单:

  1. Prototype: 这个是一个接口,里面必须含有一个可以克隆自己的方法。在Java中,我们可以使用JDK自带的java.lang.Cloneable接口来替代此接口。
  2. ConcretePrototype:实现了Prototype接口的原型对象

原型模式主要用于对象的复制,它的核心是就是类图中的原型类 Prototype。Prototype 类需要具备以下两个条件:

  • 实现 Cloneable 接口。在 java 语言有一个 Cloneable 接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用 clone 方法。在 java 虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出 CloneNotSupportedException 异常。
  • 重写 Object 类中的 clone 方法。Java 中,所有类的父类都是 Object 类,Object 类中有一个 clone 方法,作用是返回对象的一个拷贝,但是其作用域 protected 类型的,一般的类无法调用,因此,Prototype 类需要将 clone 方法的作用域修改为 public 类型。

程序化示例:

第一步,Prototype抽象原型类,声明了copy方法,它可以是接口或基类

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Prototype<T> implements Cloneable {

@SuppressWarnings({"unchecked"})
public Prototype<T> copy() {
try {
return (Prototype<T>) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}

}

第二步,原型类

这个类就是我们的原型类,准备被其他人克隆使用的,所以其实现Prototype 接口,具备克隆的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class CloneSheep extends Prototype<CloneSheep> {

private String name;

private int age;

private String color;

public CloneSheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}

}

第三步,创建原型类的工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CloneSheepFactory {

private final CloneSheep cloneSheep;

public CloneSheepFactory(CloneSheep cloneSheep) {
this.cloneSheep = cloneSheep;
}

public CloneSheep createCloneSheep() {
return (CloneSheep) cloneSheep.copy();
}

}

测试:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {

CloneSheepFactory sheepFactory = new CloneSheepFactory(new CloneSheep("多莉", 7, "白色"));
CloneSheep cloneSheep1 = sheepFactory.createCloneSheep();
CloneSheep cloneSheep2 = sheepFactory.createCloneSheep();
CloneSheep cloneSheep3 = sheepFactory.createCloneSheep();
System.out.println(cloneSheep1);
System.out.println(cloneSheep2);
System.out.println(cloneSheep3);

}

程序输出:

1
2
3
CloneSheep{name='多莉', age=7, color='白色'}
CloneSheep{name='多莉', age=7, color='白色'}
CloneSheep{name='多莉', age=7, color='白色'}

使用场景

  • 当一个对象的构建代价过高时。例如某个对象里面的数据需要访问数据库才能拿到,而我们却要多次构建这样的对象。
  • 当构建的多个对象,均需要处于某种原始状态时,就可以先构建一个拥有此状态的原型对象,其他对象基于原型对象来修改。

已知用途

  • java.lang.Object#clone()

Java Supplier 是一个代表结果提供者的函数式接口。 Supplier的函数方法是get()Supplier 可以使用 lambda 表达式或方法引用或默认构造函数进行实例化。 Supplier 已在 Java 8 中引入,属于 java.util.function 包。 Supplier函数式接口源码如下。

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Supplier<T> {

/**
* Gets a result.
*
* @return a result
*/
T get();
}

Supplier是Java8配合Lambda表达式和函数式接口编程组合使用的一个接口。

接口Supplier<T> 最适合表示工厂。带有Supplier<T> 的方法,通常应该限制输入工厂的类型参数使用有限制的通配符类型,以便客户端可以传入工厂,来创建制定类型的任意子类。

简而言之,Supplier<T>就是来创建对象的。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {

Supplier<Double> doubleSupplier = Math::random;
Double random1 = doubleSupplier.get();
Double random2 = doubleSupplier.get();
Double random3 = doubleSupplier.get();
System.out.println(random1);
System.out.println(random2);
System.out.println(random3);

}

Out:

使用Lambda表达式实例化Supplier

函数式接口Supplierget()仅仅返回值,而不接受任何参数,所以我们使用lambda表达式时其参数部分为空。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Supplier<String> strSupplier = () -> "hello world";
System.out.println(strSupplier.get());

Random random = new Random();
Supplier<Integer> integerSupplier = () -> random.nextInt(100);
System.out.println(integerSupplier.get());
System.out.println(integerSupplier.get());
}

Out:

1
2
3
hello world
65
54

使用方法引用实例化Supplier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
Supplier<Person> personSupplier = Person::new;
Person person1 = personSupplier.get();
person1.setName("张三");
Person person2 = personSupplier.get();
person2.setName("李四");
System.out.println(person1);
System.out.println(person2);

Random random = new Random();
Supplier<Integer> randomSupplier = random::nextInt;
System.out.println(randomSupplier.get());
System.out.println(randomSupplier.get());
}

Out:

1
2
3
4
com.design.patterns.factory.Person{name='张三', sex='null', age='null'}
com.design.patterns.factory.Person{name='李四', sex='null', age='null'}
-1841962637
567349876
0%