阿里巴巴开源的Java诊断工具
https://arthas.aliyun.com/doc/commands.html
安装
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后,会展示机器上的java进程,输入序号,即可attach到对应的进程
卸载
rm -rf ~/.arthas/
rm -rf ~/logs/arthas/
使用
dashboard 概要展示目标jvm的线程、内存、gc、vm,tomcat信息
展示当前进程的简要报告
类似于
$ dashboard
ID NAME GROUP PRIORI STATE %CPU TIME INTERRU DAEMON
17 pool-2-thread-1 system 5 WAITIN 67 0:0 false false
27 Timer-for-arthas-dashb system 10 RUNNAB 32 0:0 false true
11 AsyncAppender-Worker-a system 9 WAITIN 0 0:0 false true
9 Attach Listener system 9 RUNNAB 0 0:0 false true
3 Finalizer system 8 WAITIN 0 0:0 false true
2 Reference Handler system 10 WAITIN 0 0:0 false true
4 Signal Dispatcher system 9 RUNNAB 0 0:0 false true
26 as-command-execute-dae system 10 TIMED_ 0 0:0 false true
13 job-timeout system 9 TIMED_ 0 0:0 false true
1 main main 5 TIMED_ 0 0:0 false false
14 nioEventLoopGroup-2-1 system 10 RUNNAB 0 0:0 false false
18 nioEventLoopGroup-2-2 system 10 RUNNAB 0 0:0 false false
23 nioEventLoopGroup-2-3 system 10 RUNNAB 0 0:0 false false
15 nioEventLoopGroup-3-1 system 10 RUNNAB 0 0:0 false false
Memory used total max usage GC
heap 32M 155M 1820M 1.77% gc.ps_scavenge.count 4
ps_eden_space 14M 65M 672M 2.21% gc.ps_scavenge.time(m 166
ps_survivor_space 4M 5M 5M s)
ps_old_gen 12M 85M 1365M 0.91% gc.ps_marksweep.count 0
nonheap 20M 23M -1 gc.ps_marksweep.time( 0
code_cache 3M 5M 240M 1.32% ms )
Runtime
os.name Mac OS X
os.version 10.13.4
java.version 1.8.0_162
java.home /Library/Java/JavaVir
tualMachines/jdk1.8.0
_162.jdk/Contents/Hom
e/jre
thread 显示线程信息,线程堆栈
展示ID为1的线程栈(通常为主线程)
$ thread 1 | grep 'main('
at demo.MathGame.main(MathGame.java:17)
展示当前最忙的前N个线程并打印堆栈
thread -n 3
没有参数时,显示第一页线程的信息
默认按CPU增量时间降序排列
显示所有的线程
thread --all
找出当前阻塞其他线程的线程
thread -b
指定采样时间间隔
thread -i
- thread -i 1000: 统计最近1000ms内的线程CPU时间
- thread -n 3 -i 1000: 列出1000ms内最忙的3个线程栈
查看指定状态的线程
thread --state
可选值:NEW, RUNNABLE, TIMED_WAITING, WAITING, BLOCKED, TERMINATED
jad 反编译
jad demo.MathGame
watch 显示特定方法调用的输入/输出参数,返回值,抛出的异常
表达式类型
target : the object
clazz : the object's class
method : the constructor or method
params : the parameters array of method
params[0..n] : the element of parameters array
returnObj : the returned object of method
throwExp : the throw exception of method
isReturn : the method ended by return
isThrow : the method ended by throwing exception
#cost : the execution time in ms of method invocation
eg. 显示demo.MathGame#primeFactors的返回值
watch demo.MathGame primeFactors returnObj
监听程序入参和返回值
原始程序
public class UtilController {
public RespVo<List<PathUrlVo>> getUrl(@Validated @RequestBody GetUrlReqVo req) {
Date expire = Date.from(LocalDateTime.now().plusMinutes(10L).atZone(ZoneId.systemDefault()).toInstant());
final List<PathUrlVo> list = req.getPaths().stream().filter(StringUtils::isNotBlank).map(t -> {
final URL url = ossTemplate.generatePresignedUrl(t, expire);
return new PathUrlVo(t, url.toString());
}).collect(Collectors.toList());
return RespUtil.success(list);
}
}
监听参数和返回值
[arthas@20431]$ watch controller.UtilController getUrl '{params, returnObj}' -x 3
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 102 ms, listenerId: 17
method=controller.UtilController.getUrl location=AtExit
ts=2021-03-31 09:46:50; [cost=14.647368ms] result=@ArrayList[
@Object[][
@GetUrlReqVo[
paths=@ArrayList[isEmpty=false;size=1],
],
],
@RespVo[
code=@Integer[0],
message=@String[操作成功],
data=@ArrayList[
@PathUrlVo[PathUrlVo(path=apply/20210308/e0f93d12fa6f40298844f5203a0b7cb0.png, url=http://xxx.oss-cn-hangzhou.aliyuncs.com/apply/20210308/e0f93d12fa6f40298844f5203a0b7cb0.png?Expires=1617155810&OSSAccessKeyId=xxx&Signature=ruZfoV55FP%2F2Svi06dbLXyU91kY%3D)],
],
debug=null,
],
]
sc 搜索JVM加载的所有类
Search-Class
$ sc -d *MathGame
class-info demo.MathGame
code-source /root/math-game.jar
name demo.MathGame
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name MathGame
modifier public
annotation
interfaces
super-class +-java.lang.Object
class-loader +-jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@560eeb6
classLoaderHash c387f44
sm 查看已加载类的方法信息
Search-Method
只能看到由当前类所声明的方法,父类则无法看到
trace 方法调用链路,并输出方法路径上每个节点的耗时
主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路
-n, --limits x 执行次数限制
--skipJDKMethod true/false 是否跳过jdk方法追踪(默认false)
$ trace server.controller.UtilController getUrl -n 1
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 118 ms, listenerId: 2
`---ts=2021-03-31 15:38:56;thread_name=http-nio-8080-exec-4;id=48;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@279c4e3b
`---[116.384616ms] server.controller.UtilController$$EnhancerBySpringCGLIB$$d248af01:getUrl() [throws Exception]
+---[116.24846ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #95 [throws Exception]
| `---[26.746788ms] server.controller.UtilController:getUrl() [throws Exception]
| +---[0.028009ms] server.router.BizRouterContext:get() #105
| +---[26.275524ms] server.router.global.AuthTokenCheckBizRouter:process() #106 [throws Exception]
| `---throw:common.exception.SystemException #145 [code:401, msg:无效的Token]
`---throw:common.exception.SystemException #145 [code:401, msg:无效的Token]
stack 输出当前方法被调用的调用路径
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令
mc 内存编译器
retransform 加载外部class文件
mc: 在内存里,将java文件编译成字节码和class文件
retransform 加载外部class文件,retransform JVM中已加载的类
retransform指定的class文件
$ 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
$ retransform -l
Id ClassName TransformCount LoaderHash LoaderClassName
1
删除指定retransform
#删除指定retransform entry
retransform -d 1
#删除所有
retransform --deleteAll
显示触发retransform
$ retransform --classPattern demo.MathGame
retransform success, size: 1, classes:
demo.MathGame
对于同一个类,当存在多个retransform entry时,如果显示触发,则最后添加的entry生效(id最大的)
消除retransform影响
如果对某个类执行retransform之后,想要消除影响,则需要:
- 删除这个类对应的retransform entry
- 重新触发retransform
如果不清除掉所有的retransform entry,并重新触发retransform,则arthas stop时,retransform过的类仍然生效
结合jad/mc命令使用
#jad命令反编译,之后可以使用其他编辑器,如vim来修改源码
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
#mc命令来内存编译修改过的代码
mc /tmp/UserController.java -d /tmp
#加载新的字节码
retransform /tmp/com/example/demo/arthas/user/UserController.class
上传class文件到服务器的技巧
使用mc命令来编译jad的反编译的代码有可能失败。可以在本地修改代码,编译好后再上传到服务器上。有的服务器不允许直接上传文件,可以使用base64命令来绕过
# 1. 在本地先转换class文件为base64,再保存为result.txt
base64 Test.class result.txt
# 2.到服务器上,新建并编辑result.txt,复制本地的内容,粘贴并保存
# 3.把服务器上的result.txt还原为.class
base64 -d result.txt Test.class
# 4.用md5命令计算哈希值,校验是否一致
retransform限制
- 不允许新增field/method
- 正在运行的函数,没有退出不能生效
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());
}
}
sc retransform实例
# 1.启动math-game
wget https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
java -jar demo-arthas-spring-boot.jar
# 2.启动arthas-boot
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 3.访问接口,会有错误信息
curl http://localhost/user/0
# 4.使用jad反编译UserController
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
# 5.编辑UserController
vim /tmp/UserController.java
:<<!
@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);
}
!
# 6.使用sc找到加载UserController的classLoader,这里得到hash值:1be6f5c3
sc -d *UserController | grep classLoaderHash
# 7. 使用mc(内存编译器)命令编译到/tmp目录
mc -c 1be6f5c3 /tmp/UserController.java -d /tmp
# 8. 使用retransform加载新编译的UserController.class
retransform /tmp/com/example/demo/arthas/user/UserController.class
# 9.访问接口,正常返回
curl http://localhost/user/0
# 10.查看retransform项
retransform -l
# 11.删除retransform项
retransform -d 1
# 12.显式触发retransform
retransform --classPattern com.example.demo.arthas.user.UserController
tt 方法执行数据的时空隧道
记录指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
TimeTunnel,记录当时方法调用的所有入参和返回值、抛出的异常
- ThreadLocal信息丢失,无法通过Arthas保存
- 引用的对象
tt是将当前环境的对象引用保存起来,如果方法内部对入参进行了变更,或返回的对象经过了后续处理,在tt查看的时候将无法看到当时准确的值
记录当前方法的调用现场:
参数:-t 记录方法调用;-n 3,指定记录的次数
tt -t demo.MathGame primeFactors
#解决方法重载
tt -t *Test print params.length==1
tt -t *Test print 'params[1] instanceof Integer'
#解决指定参数
tt -t *Test print params[0].mobile=="13989838402"
查看调用记录
$ 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
查看调用信息
tt -i 1002
重做一次调用
tt -i 1002 -p
logger
查看logger信息,更新logger level
[arthas@22033]$ logger
name root
class org.apache.logging.log4j.core.async.AsyncLoggerConfig
classLoader sun.misc.Launcher$AppClassLoader@18b4aac2
classLoaderHash 18b4aac2
level INFO
config XmlConfiguration[location=/Users/qudian/IdeaProjects/external/gateway/gw-server/target/classes/log4j2-spring-local.xml]
additivity true
codeSource file:/Users/qudian/.m2/repository/org/apache/logging/log4j/log4j-core/2.13.3/log4j-core-2.13.3.jar
appenders name console
class org.apache.logging.log4j.core.appender.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@18b4aac2
classLoaderHash 18b4aac2
target SYSTEM_OUT
name lci.gw.dao
class org.apache.logging.log4j.core.config.LoggerConfig
classLoader sun.misc.Launcher$AppClassLoader@18b4aac2
classLoaderHash 18b4aac2
level DEBUG
config XmlConfiguration[location=/Users/qudian/IdeaProjects/external/gateway/gw-server/target/classes/log4j2-spring-local.xml]
additivity false
codeSource file:/Users/qudian/.m2/repository/org/apache/logging/log4j/log4j-core/2.13.3/log4j-core-2.13.3.jar
appenders name console
class org.apache.logging.log4j.core.appender.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@18b4aac2
classLoaderHash 18b4aac2
target SYSTEM_OUT
更新logger level
[arthas@22033]$ logger -n lci.gw.dao --level debug
Update logger level success.
vmtool
利用JVMTI接口,实现查询内存对象,强制 GC 等功能
# 获取对象
vmtool --action getInstances --className java.lang.String --limit 10
# 指定 classloader hash
#可以通过sc命令查找到加载 class 的 classloader
sc -d org.springframework.context.ApplicationContext
# 然后用-c/--classloader 参数指定
# 指定返回结果展开的层数
vmtool --action getInstances -c 19469ea2 --className org.springframework.context.ApplicationContext -x 2
# 执行表达式
# 调用UserController#findUserById
vmtool --action getInstances --className com.example.demo.arthas.user.UserController --express 'instances[0].findUserById(1)'
# 强制GC
vmtool --action forceGc
# interrupt 指定线程
vmtool --action interruptThread -t 1
mbean 查看MBean的信息
ognl 执行ognl表达式
调用静态方法
ognl '@java.lang.System@out.println("hello")'
获取静态类的静态字段
ognl '@demo.MathGame@random'
dump dump已加载类的bytecode到特定目录
heapdump 类似jmap命令的heap dump
classloader 查看classloader的继承树,urls,类加载信息
monitor 方法执行监控
非实时返回命令,服务端是以人物的形式在后台跑任务
profiler 使用async-profiler生成火焰图
重新连接
telnet 127.0.0.1 3658
退出
quit 或者 exit
与Skywalking-agent兼容问题
在启动命令上,添加参数:
-Dskywalking.agent.is_cache_enhanced_class=true -Dskywalking.agent.class_cache_mode=MEMORY
JSON形式输出
控制台输入命令
options json-format true
即可让结果、参数以json的方式输出。
比如用watch命令查看响应值,输出的形式就是json