zoukankan      html  css  js  c++  java
  • 阿里开源Java诊断工具--Arthas入门

    一、简介

    Arthas是Alibaba开源的一款Java诊断工具,采用命令式交互模式,用来排查各种JVM的问题。

    Arthas主要提供了一下几种功能

    1、实时监控JVM运行状况

    2、实时查看已加载的类型和类加载器信息

    3、通过字节码增强技术实现方法执行的监控和统计

    二、Arthas的使用

    2.1、Arthas安装启动

    Arthas本质上也是一个Java程序,没有安装流程,只需要下载jar包,通过java命令直接启动即可

    下载arthas-boot.jar,然后用java -jar的方式启动:

    下载命令:

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

    启动命令:

    java -jar arthas-boot.jar

    启动之后会列出当前节点正在运行的所有Java进程,并且有对应的序号,可以输入序号来选择需要访问的进程信息,如选择第一个进程就直接输入1即可,启动效果如下图示:

    表示Attach上进程号为21093的Java进程,并且此时就进入Arthas的命令交互模式,不可以再输入操作系统的命令,只可以输入Arthas相关的命令,可以输入help命令查看所有Arthas支持的命令,如下:

    2.2、Arthas命令概览

    Arthas命令主要分成几个类型,分别是基本命令、JVM相关命令、class相关命令、监控相关命令

    2.2.1、基本命令

    命令  用途
    help 查看命令帮助信息
    cat 打印文件内容
    echo 打印参数
    grep 匹配查找
    base64 base64编码转换
    tee 复制标志输入到标准输出和指定的文件
    pwd 返回当前工作目录
    cls 清空屏幕内容
    session 查看当前会话信息
    reset 重置增强类,将被Arthas增强过的类全部还原,Arthas服务关闭时自动还原
    version 输出当前目标Java进程加载的Arthas版本号
    history 打印命令历史
    quit 退出当前Arthus客户端
    stop 关闭Arthas服务端,所有Arthas客户端全部退出

    2.2.2、JVM相关命令

    命令 用途 用法
    dashboard 打印当前JVM实时数据面板 dashboard
    thread 打印当前JVM线程堆栈信息

    thread

    thread 线程ID

    thread --state WAITING 

    jvm

    打印当前JVM实时运行信息 jvm
    sysprop 打印和修改当前JVM信息 sysprop
    sysenv  查看当前JVM环境变量 sysenv
    vmoption 查看和修改JVM里诊断相关option vmoption
    logger 查看和修改logger信息 logger
    getstatic 获取静态变量的值 getstatic [className] [staticField]
    ognl 执行ognl表达式 ognl 表达式
    mbean 打印MBean信息 mbean
    heapdump 打印堆栈信息

    heapdump

    heapdump /test/dump/test.hprof

    heapdump --live /test/dump/test.hprof

    vmtool 从jvm里查询对象,执行forceGc vmtool --action getInstances --className [className]
    perfcounter 查看当前JVM的 Perf Counter信息 perfcounter

    2.2.3、class相关命令

    命令 用途 用法
     sc  查询JVM已加载的类信息

     sc  *[className]*

     sc -d *[className]*

    sc -d -f *[className]*

     sm 查询JVM已加载的方法信息 

     sm [className]

    sm -f [className]

     jad 反编译JVM已加载的类信息   jad [className]
     mc  内存编译器,通过内存编译.java文件

     mc /com/test/test.java

    mc -c [类加载ID] /com/test/test.java

    mc --classLoadClass [classLoadClassName] /com/test/test.java

    mc -d /tmp/file /com/test/test.java 

     retransform  加载外部.class文件,retransform到JVM中  retransform /com/test/test.class
     redefine  加载外部.class文件,redefine到JVM中 redefine /com/test/test/class 
     dump  dump已加载类的字节码到指定目录

     dump java.lang.String

    dump -d /tmp/file java.lang.String

     classloader 查看类加载器继承树信息 

     classloader

    classloader -l

    classloader -t

    classloader -c [类加载ID] --load [className]

    2.2.4、监控相关命令

    监控相关的命令是通过字节码增强技术将增强的逻辑织入到目标类中,所以监控完成之后需求及时执行reset命令去除增强的逻辑

    命令 用途 用法
    watch 方法执行数据观测 watch [className] [methodName] "{params,returnObj}" -x 2
    monitor 方法执行监控 monitor -c 5 [className] [methodName]
    stack 输出当前方法被调用的路径 stack [className] [methodName]
    trace 方法内部的调用路径,并打印路径耗时 trace [className] [methodName]
    tt 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 tt -t [className] [methodName]

    2.3、Arthas命令详解

    2.3.1、dashborad(实时看板) 

    语法:dashboard [i:] [n:]

    dashboard  ##默认每个5秒打印一次JVM实时数据
    dashboard -i 2000  ##每隔2秒打印一次JVM实时数据
    dashboard -n 10  ##总共打印10次JVM实时数据
    dashboard -i 1000 -n 10 ##每隔1秒打印一次JVM实时数据,共打印10次

    dashboard命令用于实时查看JVM运行信息,包括三个模块分布是线程运行情况、内存使用情况以及JVM运行环境信息等

    线程模块会打印当前JVM所有的线程运行状况,包括线程ID、名称、优先级、状态、占有CPU比率、占有CPU时长等;

    内存模块会打印JVM当前堆内和堆外内存使用情况以及GC的次数和时间统计

    运行环境模块会打印当前操作系统和JDK的版本信息以及运行环境等信息

    2.3.2、thread(线程信息)

    通过thread命令可以打印当前所有运行的线程信息,并且可以通过thread 线程ID的方式查看指定线程的栈信息

    语法

    thread 查询所有线程

    thread <id>  查询指定线程

    thread -n <n>  查询最忙的n个线程

    thread -i <i> : 统计最近指定毫秒内的线程CPU时间

    thread -n <n> -i <i> : 列出最近指定毫秒内最忙的N个线程栈

    thread -b 查询阻塞其他线程的线程

    thread --state [RUNNABEL]|[WAITING]|[TIMED_WAITING]|[NEW]|[BLOCKED]|[TERMINATED]  查询指定状态的线程

    thread   ## 查看所有线程信息
    thread <id> ## 查看指定线程ID的信息
    thread -n 10 ## 查看最忙的10个线程
    thread -b ## 查看阻塞其他线程的线程信息
    thread --state WAITING ## 查看等待状态的线程信息

    通过thread查看所有线程信息,除了业务线程还会包含JVM内部线程,JVM内部线程ID为-1,包括GC线程如GC task thread#2 (ParallelGC)、JIT编译线程如C2 CompilerThread0、其他内部线程如VM Periodic Task Thread等

    thread -b 命令可以找出当前阻塞了其他线程的线程,比如线程A通过Synchronized关键字拿到了锁,线程B和线程C被阻塞了,那么此时就可以通过thread -b命令查询出来,目前也仅支持Synchronized获取锁阻塞的情况

    2.3.3、jvm(查看JVM信息)

    用法:jvm

    jvm命令可以打印当前JVM的实时信息,包括运行环境、加载的类统计、内存使用情况、GC统计情况、线程统计情况、文件描述符统计信息等

    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当前打开的文件描述符数

    2.3.4、jad(反编译)

    语法

    jad <类完整路径>  反编译指定类

    jad <类完整路径> <方法名> 反编译指定方法

    通过sc可以查看所有已加载的类信息,然后就可以通过jad命令可以将已加载的类.class文件进行反编译,命令如下:

    jad com.test.ArthasDemo

    2.3.5、sc(查看已加载的类)

    语法

    sc [-d] [-f] *<className>*  

    [-d] 表示打印详细信息;[-f]表示打印属性信息,可以通过关键字模糊查询

    sc是search class的缩写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息,这个命令支持的参数有 [d][E][f] 和 [x:],并且支持模糊查询

    如想搜索后缀为Service的类,则可以输入一下命令:

    sc -d -f *Service

    -d表示输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次

    -f表示输出当前类的成员变量信息(需要配合参数-d一起使用)

    通过该命令可以查看已加载的类的详细信息,包括code-source表示从哪个包中加载的,使用哪个class-loader类加载器加载的等,案例如下图示:

    2.3.6、sm(查看已加载类的方法)

    用法

    sm <类完整路径>

    sm -d <类完整路径>

    sm -d <类完整路径> <方法名>

    sm命令可以查看已加载的类的函数,比如查看java.math.RoundingMode类的所有方法,则命令如下:

    sm -d java.math.RoundingMode

    2.3.7、getstatic(查看静态属性)

    用法

    getstatic <类完整路径> <静态属性名>

    通过getstatic命令可以查看已加载的类的静态属性的值

    2.3.8、watch(监控方法执行数据)

    用法

    watch <className> <method> 观察指定类指定方法的执行情况,返回耗时,返回值等信息

    watch <className> <method> "{params,returnObj}"-x <x> 观察入参和出参,返回结果便利深度为x,默认深度为1

    watch <className> <method> "{params,returnObj}"-b 观测方法调用前的入参和返回值,调用方法前返回值肯定为空

    watch <className> <method> "{params,returnObj}" -b -s -n <n> 同时观察方法执行前和方法执行后的入参和返回值,监控n次

    watch <className> <method> "{params,throwExp}" -e 观察方法在抛出异常时的入参和异常信息

    watch <className> <method> "{params,target}" -b -s 观察方法执行前和方法执行后入参和当前对象的信息

    watch <className> <method> "{params,target.fieldName}" -b -s 观察方法执行前和方法执行后入参和当前对象的fieldName属性的值

    watch <className> <method> "{params}" '#cost>100' -f 观察方法执行后执行耗时大于100毫秒的入参信息

    通过watch命令可以监控指定类的指定方法的参数、返回值和异常信息,watch 命令定义了4个观察事件点,即 -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后,

    4个观察事件点 -b-e-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出

    这里要注意方法入参方法出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表方法入参外,其余事件都代表方法出参

    当使用 -b 时,由于观察事件点是在方法调用前,此时返回值或异常均不存在

    2.3.9、monitor(方法执行监控)

    用法

    monitor -c <second> <className> <methodName> 定时second秒统计一次指定类指定方法的调用情况

    monitor -c <second> -b <className> <methodName> 'params[1] > 1' 方法调用之前统计第二个参数大于1的调用情况

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

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

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

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

    monitor和watch的区别是watch是实时监控,每次方法调用都会打印;monitor是统计之后定时打印,watch倾向于实时查看,monitor倾向于定时统计

    monitor可以监控的维度包括:timestamp(时间戳)、class(类)、method(方法)、total(调用次数)、success(成功次数)、fail(失败次数)、rt(平均RT)、fail-rate(失败率)

    monitor结果如下图示:

     

    2.3.10、trace(方法调用路径)

    用法

    trace <className> <methodName> 实时打印指定类指定方法的调用链路

    trace <className> <methodName> --skipJDKMethod false 打印JDK方法执行链路,默认会被过滤

    trace <className> <method> '#cost > 100' 过滤耗时大于100毫秒的调用链路

    trace <className> <method> '#cost > 100' -n <n> 限制实时监控的次数为n次

    trace可以查询指定类的指定方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路

     打印信息中包含了整个链路的每一步,并且打印了每一个链路的代码行数和耗时情况

    2.3.11、stack(输出当前方法被调用的路径)

    用法

    stack <className> <methodName> 打印某个方法被调用的链路

    stack <className> <methodName> 'params[0] > 1' 按入参条件过滤

    stack <className> <methodName> '#cost > 100' 按耗时进行过滤

    当一个方法可以被很多地方调用时,想要查询当前方法是被哪个方法调用,此时就可以通过stack命令得知当前方法是从什么地方被执行的,如下图示

     

    3、Arthas使用案例

    案例一:排查方法执行异常

    场景:线上某个接口偶尔会报500错误,此时就需要排查报500错误时的具体参数

    可以通过watch命令监控接口报500错误时的参数和异常信息命令如下:

    watch <类路径,支持通配符> <方法名,支持通配符> "{params, throwExp}" -e 表示当发生异常时打印出参数和异常信息内容


    案例二:热更新代码

    场景:线上存在一个小BUG,需要修改部分代码就可以修复,而发版成本较大时就可以通过热更新的方式替换线上的代码逻辑

    热更新涉及到几个步骤,

    1、首先需要将.class文件进行反编译成源代码文件

    2、然后通过编辑vim编辑源代码文件更新代码逻辑

    3、然后再将新源代码文件编译成.class文件

    4、最后再将新的.class文件替换掉JVM中的已加载的.class文件

    假设用一个UserController的getUserDetail代码如下:

    1 @RestController
    2 @RequestMapping(value = "/testUser")
    3 public class TestController {
    4 
    5     @RequestMapping(value = "/detail", method = RequestMethod.GET)
    6     public String getUserName(@RequestParam("userId") String userId){
    7         return "name is :" + Long.valueOf(userId);
    8     }
    9 }

    由于参数userId定义的是String类型,因此可能会出现第7行的转换异常,所以需要将userId类型改成Long类型,并通过热更新的方式部署

    第一步:通过jad命令反编译UserController类,并将反编译后的代码写入临时文件夹

     jad --source-only com.zjic.message.business.controller.TestController > /tmp/TestController.java

    第二步:通过vim来编辑TestController.java文件,修改源代码将入参的String类型改成Long类型

    vim /tmp/TestController

    第三步:编译新的TestController.java文件

    编译UserController可以指定原先的类加载器编译,那么可以先通过sc命令找到原先的类加载

    sc -d *TestController | grep 'classLoader'

    查到classLoader的hash码后就可以通过mc命令编译新的TestController.java文件,并写入到tmp目录生成TestController.class文件

     mc -c 17d99928 /tmp/TestController.java -d /tmp

    第四步:通过redefine命令将新生成的TestController.class替换掉JVM中已加载的TestController

    1 redefine /tmp/com/zjic/message/business/controller/TestController.class

    结果如下:

    提示删除了一个方法所以替换失败,因为通过反编译修改新的TestController.java文件时将方法的参数类型进行了修改,那么就相当于重新定义了一个方法,所以想要通过redefine替换.class文件时,类中的方法个数、参数个数、参数类型以及返回值都不允许修改,只可以修改方法内部的实现逻辑。

    重新修改TestController.java文件,不修改参数userId的类型在方法体内部添加try/catch捕获异常,

           @RestController
           @RequestMapping(value={"/testUser"})
           public class TestController {
               @RequestMapping(value={"/detail"}, method={RequestMethod.GET})
               public String getUserName(@RequestParam(value="userId") String userId)        { 
                Long id = null;
                try{
                    id = Long.parseLong(userId);
                  }catch(Exception e){ }
                   return "name is :" + id;
               }
           }

    并重新编译TestController.class文件,最后重新执行redefine命令,结果如下:

    测试接口验证通过,从而成功实现了热更新功能


    案例三:线上慢请求排查

    场景:当访问线上某个接口时,发现接口相应比较慢并且通过代码又不太好排查出具体是哪一行代码引起的慢请求

    通过trace命令可以监控接口方法的调用链路和各个链路的耗时,从而可以分析接口最耗时的代码块在哪,案例如下:

    trace com.zjic.message.business.controller.BusinessApplyController * '#cost > 1'

    先监控指定Controller的所有方法,采用*通配符匹配,然后可以发现最耗时的代码是Service层的方法,此时就可以再继续使用trace命令继续查看Service层方法的调用链路

    最终定位到最耗时的是Mapper层的方法,所以该接口最耗时的就是SQL语句的执行,那么就可以针对SQL优化来进行接口的优化

     4、Arthas实现原理

    4.1、JVM的Attach机制

    4.1.1、Attach机制实现原理

    想要了解Arthas的实现原理,首先需要理解JVM的attach机制,attach机制的主要功能就是实现了一个JVM进程和另一个JVM进程之前相互发送命令进行通信的机制。

    JVM运行时的相关状态数据只有JVM本身掌握,此时想要掌握只能通过访问JVM来查询,而attach机制就相当于JVM对其他JVM进程开放了一个对接入口,其他JVM进程只要attach到当前JVM就可以发生命令让当前JVM执行。

    常见的jstack、jmap、debug等操作都是通过attach机制来实现的。

    attach机制实现的关键是两个JVM线程,分别是Attach Listener线程和Signal Dipatcher线程

    Attach Listener线程负责接收外部发送来的JVM命令,并处理JVM命令返回结果

    Signal Dispatcher线程负责将分发到JVM信号,然后将结果返回

    Attach Listener线程并非是随着JVM启动而启动的,而是需要显示在启动JVM时启动,或者当第一个JVM命令到来时才启动;而Signal Dispatcher线程是随着JVM启动就是创建启动

    当外部进程Attach目标JVM时,会向目标进程发送sigquit信号,目标进程接收到信号之后广播给子线程,而只要Signal Dispatcher线程会处理该信号,并会创建Attach Listener线程

    Attach Listener线程启动之后,会创建监听套接字文件/tmp/.java_pid,表示外部进程Attach目标JVM成功。之后外部进程发送命令写入该套接字,Attach Listener线程监听该套接字,解析成功命令进行处理。

    4.1.2、Attach机制使用

    JDK提供了VirtualMachine类可以用于attach目标JVM,可以直接调用VirtualMachine的attach(pid)方法attach到目标JVM,获取到目标JVM的虚拟机对象,代码如下:

    /** attach进程ID为1357的JVM进程,获取虚拟机对象 */
    VirtualMachine virtualMachine = VirtualMachine.attach("1357");

    通过attach获取到虚拟机VirtualMachine对象之后,就可以直接通过VirtualMachine提供的API访问JVM,例如查询JVM环境参数、内存dump、线程dump等数据,实际就是向JVM发送指定的命令,VirtualMachine的子类为HotSpotVirtualMachine

    部分方法如下:

       /** 执行datadump命令 */ 
       public void localDataDump() throws IOException {
            this.executeCommand("datadump").close();
        }
       
        /** 执行threaddump命令 */
        public InputStream remoteDataDump(Object... var1) throws IOException {
            return this.executeCommand("threaddump", var1);
        }
    
        /** 执行dumpheap命令 */
        public InputStream dumpHeap(Object... var1) throws IOException {
            return this.executeCommand("dumpheap", var1);
        }
    
        /** 执行inspectheap命令 */
        public InputStream heapHisto(Object... var1) throws IOException {
            return this.executeCommand("inspectheap", var1);
        }

    而常用的jstack命令实际就是调用了HotSpotVirtualMachine的remoteDataDump方法实现的,jmap命令实际激素调用了HotSpotVirtualMachine的dumpHeap方法实现的。

    4.2、Java agent

    Arthas命令中和查询相关的命令基本上就可以通过Attach机制来实现,但是Arthas命令中还包括了一些操作的命令,比如热部署相关的命令等,

    这些命令可以实现修改JVM中已经加载的class的功能。这些通过Attach机制是无法实现的,此时就可以通过Java agent来实现Java agent也可以叫做Java探针,是一种可以动态修改字节码的技术。

    Java类的运行是需要先将Java类编译成字节码,然后将字节码加载到JVM中运行的,而Java agent实现的功能就是可以动态的修改已经加载到JVM中的字节码从而实现动态扩展的效果。Java agent可以在JVM加载字节码之前修改也可以在JVM加载之后修改。

    Java agent修改字节码有两个时机,一个是在执行main方法之前的premain方法中添加拦截逻辑,当字节码加载时进行拦截和修改;另一个就是在JVM运行期间修改,此时就需要通过Attach机制来实现,可以调用VirtualMachine的loadAgent方法加载agent逻辑

    想要继续了解Java agent实现原理之前就首先需要了解JVMTI和JVMTIAgent

    JVMTI

    JVMTI是JVM ToolInterface,直译就是JVM工具接口,顾名思义就是JVM提供给用户的一套工具接口,用户可以通过访问JVMTI相关接口来访问JVM。

    JVMTI是基于事件驱动的,当JVM触发了特定的事件时(如加载class)会调用该事件对应的回调函数,而回调函数就可以用来进行功能扩展。

    JVMTIAgent

    JVMTI是一套本地代码接口,因此想要使用JVMTI就需要和C/C++打交道,所以就需要有一个代理来负责和JVMTI交互,Java程序只需要和代理交互即可,这个代理就是JVMTIAgent。将代理编译成动态链接库之后就可以在JVM启动时加载,也可以在JVM运行期间加载。

    JVMTIAgent主要提供了三个方法,分别是Agent_OnLoad、Agent_OnAttach、Agent_OnUpload

    Agent_OnLoad:如果agent在启动时加载就执行此方法;

    Agent_OnAttach:如果agent在attach某个JVM进程后发送loadAgent命令时就调用此方法

    Agent_OnUpload:当agent被卸载时会调用

    Instrument

    Instrument就是JDK6提供的一个JVMTIAgent动态链接库,也就是一个.dll,并且实现了Agent_OnLoad和Agent_OnAttach方法,所以Instrument提供了agent在JVM启动时和JVM运行时两个时机加载agent的方法

    启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式

    运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载

    工作流程

    1、JVM启动时先加载agent,找到动态链接库dll,其中就包含了Instrument对应的dll

    2、执行Instrument的Agent_OnLoad方法

    3、Instrument的Agent_OnLoad方法会创建一个Instrumentation接口的实例InstrumentationImpl对象

    4、并监听ClassFileLoadHook事件,也就是类被加载的事件

    5、然后调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法

    6、premain方法可以获取到InstrumentationImpl实例,JVM启动时执行agent的Agent_OnLoad方法,该方法会创建一个Instrumentation接口的实例InstrumentationImpl对象,然后监听ClassFileLoadHook(类加载事件),

    调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法

  • 相关阅读:
    yum插件yum-fastestmirror
    mysql利用yum安装指定数据存放路径
    快速搭建Seeddms文档管理系统
    Oracle单实例启动多个实例
    HTTP 304状态分析
    Oracle快速克隆安装
    Linux安装SQLite轻量级数据库
    redhat利用yum快速搭建LAMP环境
    将博客搬至CSDN
    GenericServlet 、Servlet和httpServler他们之间的关系
  • 原文地址:https://www.cnblogs.com/jackion5/p/15118597.html
Copyright © 2011-2022 走看看