Arthas入门到精通

一、简介

Arthas是Alibaba开源的一款Java诊断工具,方便开发者在线排查问题,无需重启,同时可以跟踪Java代码,实时监控JVM状态,目前Arthas仅支持JDK6+,支持Linux/Mac/Windows,采用命令行交互模式,具有 Tab 自动补全功能,便于开发者进行快速定位和诊断问题。

二、安装运行

1、arthas下载

  • windows下载

https://arthas.aliyun.com/arthas-boot.jar

  • Linux下载
wget https://arthas.aliyun.com/arthas-boot.jar
  • 离线全量下载(如果服务器没有外网可以采用这种情况)

https://arthas.aliyun.com/download/latest_version?mirror=aliyun

2、安装测试案例并运行

#下载运行测试案例
wget https://arthas.aliyun.com/math-game.jar
#注:不要ctrl c 再打开一个ssh连接,不然程序会关掉
nohup java -jar math-game.jar

3、运行arthas

# 运行arthas 下面会自动检测导当前正在运行的java程序,输入1或者2然后回车来选择要监控的程序
[root@VM-24-14-centos arthas]# java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.6.1
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 4559 math-game.jar
  • 注:使用openJDK

如果你使用的是openJDK会导致运行arthas-boot找不到正在运行的java实例,我们可以手动netstat -tunlp | grep "端口" 找到PID 然后使用 java -jar arthas-boot.jar PID 运行

三、命令介绍

1、dashbord

查看当前系统的实时数据面板,例如:服务器thread信息、内存memory、GC回收等情况
image.png

  • dashbord 查询实时数据面板

数据列说明

说明
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线程

2、Thread

查看线程的堆栈信息
image.png

  • **thread **查询全部线程
  • thread -n 5 打印前5个最忙的线程并打印堆栈
  • thread -all 显示所有匹配的线程
  • thread -n 3 -i 1000 列出1000ms内最忙的3个线程栈
  • thread –state WAITING 查看指定状态的线程,(TIMED_WAITI、WAITING、RUNNABLE等等)
  • thread -b 找出阻塞其他线程的线程,当出现死锁后,会提示你出现死锁的位置,代码如下

3、vmoption查看/更新虚拟机参数

这个命令可以看到我们的java项目在运行时设置了哪些参数,命令没有参数时会打印所有的vm参数
image.png

  • vmoption 查询全部虚拟数据
  • vmoption PrintGCDetails 查看指定的vm参数
  • vmoption PrintGCDetails true 更新指定的vm参数

4、sysprop

命令可以查看当前JVM的系统属性(System Property)
image.png

  • sysprop 查询全部JVM系统属性
  • sysprop java.version 查询单个属性
  • sysprop user.country CN 修改单个属性

5、getstatic

通过getstatic命令可以方便的查看类的静态属性
image.png

  • getstatic demo.MathGame random 格式为getstatic <类路径><静态属性名字>

6、sc(重点)

查看JVM已加载的类信息
image.png

  • sc demo.MathGame_ _查看的jvm信息
  • sc -d demo.MathGame 查看MathGame的详细JVM内存信息

7、jad(重点)

反编译指定已加载类的源码
image.png

  • jad demo.MathGame 将内存加载的class反编成代码和classloader信息(注:不是源代码)
  • **jad --source-only demo.MathGame **将内存加载的class反编译成java代码

8、mc(重点)

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

参考实战案例第一节

9、retransform(重点)

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

参考实战案例第一节

10、monitor(重点)

监控方法的调用次数成功次数和失败次数,平均响应时间等。

监控项 说明
timestamp 时间戳
class Java类
method 方法(构造方法、普通方法)
total 调用次数
success 成功次数
fail 失败次数
rt 平均响应时间
fail-rate 失败率
  • monitor -c 5 com.example.demo.arthas.user.UserController findUserById 每隔五秒中统计一下这个方法五秒内的调用次数等信息

11、watch(重点)

watch的作用是用来监控项目运行时方法的出入参。主要用到的参数又六个

  • 监控类的全路径:如下类的全路径demo.MathGame
  • 监控类的那个方法:如下的print方法
  • 观察表达式:如下的params,常用的还有returnObj、throwExp
  • 观察点:-b(方法调用之前)、-e(方法抛出异常之后)、-s(方法返回之后)、-f(方法结束之后)不写默认是f
  • 条件表达式:可以用来过滤指定参数的出入参
  • -x参数:-x [n],其中n表示遍历的深度。如果参数结构深可以指定深度解析
# 下载地址https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
# 运行arthas的测试用例demo-arthas-spring-boot.jar
nohup java -jar demo-arthas-spring-boot.jar > /fyx/arthas/log/mathgame.log  2>&1 &
  • 查询UserController类的findUserById 的入参
  • watch com.example.demo.arthas.user.UserController findUserById 'params'

image.png
image.png

  • 查询UserController类的findUserById 的出入参,这里虽然能看到出入参,但是只能看到类信息。
  • watch com.example.demo.arthas.user.UserController findUserById '{params,returnObj}'

image.png
image.png

  • 查询UserController类的findUserById 的出入参,-x 深度遍历查看具体的值 watch
  • watch com.example.demo.arthas.user.UserController findUserById '{params,returnObj}' -x 2

image.png
image.png

  • 查询UserController类的findUserById 的入参,并查看异常信息,如果只想看异常的入参和异常在-x 前加个-e
  • watch com.example.demo.arthas.user.UserController findUserById '{params,throwExp}'-e -x 2

image.png
image.png

  • 条件过滤查询入参=10的出入参数
  • ** watch com.example.demo.arthas.user.UserController findUserById "{params,returnObj}" "params[0]==10" -x 2**

image.png
image.png

12、trace(重点)

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

package demo;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
    private static Random random = new Random();
    private int illegalArgumentCount = 0;
    
    public List<Integer> primeFactors(int number) {
        /*44*/         if (number < 2) {
            /*45*/             ++this.illegalArgumentCount;
            throw new IllegalArgumentException("number is: " + number + ", need >= 2");
        }
        ArrayList<Integer> result = new ArrayList<Integer>();
        /*50*/         int i = 2;
        /*51*/         while (i <= number) {
            /*52*/             if (number % i == 0) {
                /*53*/                 result.add(i);
                /*54*/                 number /= i;
                /*55*/                 i = 2;
                continue;
            }
            /*57*/             ++i;
        }
        /*61*/         return result;
    }
    
    public static void main(String[] args) throws InterruptedException {
        MathGame game = new MathGame();
        while (true) {
            /*16*/             game.run();
            /*17*/             TimeUnit.SECONDS.sleep(1L);
        }
    }
    
    public void run() throws InterruptedException {
        try {
            /*23*/             int number = random.nextInt() / 10000;
            /*24*/             List<Integer> primeFactors = this.primeFactors(number);
            /*25*/             MathGame.print(number, primeFactors);
        }
        catch (Exception e) {
            /*28*/             System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
        }
    }
    
    public static void print(int number, List<Integer> primeFactors) {
        StringBuffer sb = new StringBuffer(number + "=");
        /*34*/         for (int factor : primeFactors) {
            /*35*/             sb.append(factor).append('*');
        }
        /*37*/         if (sb.charAt(sb.length() - 1) == '*') {
            /*38*/             sb.deleteCharAt(sb.length() - 1);
        }
        /*40*/         System.out.println(sb);
    }
}

  • trace demo.MathGame run (监控追踪MathGame类下面的run方法)

image.png

  • trace demo.MathGame run -n 1 (监控追踪MathGame类下面的run方法,只监控一次)

image.png

  • trace --skipJDKMethod false demo.MathGame run -n 1 (监控run所有方法,因为trace默认不追踪jdk自带的方法--skipJDKMethod false 就是开启追踪jdk默认的方法)

image.png

  • trace demo.MathGame run '#cost > 0.1' (查询大于0.1ms的执行方法)

image.png

13、stack(重点)

查询某个方法被执行的时候的调用路径

  • stack demo.MathGame primeFactors (查询primeFactors被调用的时候调用的引用顺序)

image.png

  • stack demo.MathGame primeFactors 'params[0]<0' -n 2(显示入参为0的引用路径,显示两次)

image.png

14、tt(重点)

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

watch 虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。于是乎,TimeTunnel 命令就诞生了。

  • 查询三次findUserById 方法的调用详情

**tt -t -n 3 com.example.demo.arthas.user.UserController findUserById **

image.png

  • 查看所有的tt记录

tt -l

image.png

  • 查看tt列表的详情,比如入参和出参

tt -i 1001 (1001是INDEX的值)

image.png

四、案例实战

1 、热更新代码(修改正在运行的java程序)

注:修改jvm内存的代码需要注意两个点,第一不能添加新的字段信息,第二添加的代码如果方法还在执行就不会生效

1.1、下载测试用例

# 下载测试用例
wget https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
# 运行用例
nohup java -jar demo-arthas-spring-boot.jar

1.1、把需要修改的类反编译出来

# 使用jad 将jvm的class文件反编译成.java文件保存到临时目录
jad --source-only com.example.demo.arthas.user.UserController > /fyx/tmp/UserController.java

1.2、退出arthas修改代码

# 退出arthas
stop
# 进入目录
cd /fyx/tmp 
# 修改代码
vim UserController
package com.example.demo.arthas.user;

import com.example.demo.arthas.user.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    
    @GetMapping(value={"/user/{id}"})
    public User findUserById(@PathVariable Integer id) {
        logger.info("id: {}", (Object)id);
        if (id != null && id < 1) {
            logger.info("返回的数值有问题{}",id);
            return new User(id.intValue(),"name"+id);
        }
        return new User(id.intValue(), "name" + id);
    }
}


1.3、查看UserController的类加载器的hashcode

#进入arthas查看UserController的hash码
[arthas@10181]$ sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
classLoaderHash   5674cd4d

1.4、将修改的代码编译成字节码并添加类加载器

# -c <类加载器hashcode> <需要编译的路径> -d <字节码生成的路径>
mc -c 5674cd4d /fyx/tmp/UserController.java -d /fyx/tmp/

1.5、加载更改的代码

retransform /fyx/tmp/com/example/demo/arthas/user/UserController.class

1.6、访问修改的代码

发现我们新添加的代码是有效的
image.png

1.7、增强回退

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

#查看增强的类
[arthas@10181]$ retransform -l
Id              ClassName       TransformCount  LoaderHash      LoaderClassName 
1               com.example.dem 1               null            null            
                o.arthas.user.U                                                 
                serController      
#删除指定的增强了1就是retransform -l 查看出来的id
retransform  -d 1
#重新加载类
retransform --classPattern com.example.demo.arthas.user.UserController

五、使用arthas的注意点

  1. 如果arthas使用完后一定要使用stop命令推出程序,不然下次监听其他服务的时候就会监听不到。解决方案就是重新进上一次的监听的程序再执行一次stop命令。
posted @ 2022-06-06 14:16  xiye1  阅读(413)  评论(0)    收藏  举报