GC调优基础知识工具篇之Arthas与动态追踪技术

JVM系列文章目录

初识JVM

深入理解JVM内存区域

玩转JVM对象和引用

JVM分代回收机制和垃圾回收算法

细谈JVM垃圾回收与部分底层实现

Class文件结构及深入字节码指令

玩转类加载和类加载器

方法调用的底层实现

Java语法糖及底层实现

GC调优基础知识工具篇之JDK自带工具

GC调优基础知识工具篇之Arthas与动态追踪技术

JVM调优之内存优化与GC优化

JVM调优之预估调优与问题排查

JVM调优之玩转MAT分析内存泄漏

直接内存与JVM源码分析

JVM及时编译器



前言

本文基于JDK1.8,是博主个人的JVM学习记录,欢迎各位指正错误的地方。本文主要介绍Arash的使用及其底层实现——动态追踪技术。



arthas

官方文档
Arthas 是 Alibaba 开源的 Java 诊断工具, 支持 JDK 6+。

下载安装

Arthas其实就是个jar包,我们从官网下载到本地后解压后,可以通过java -jar arthas-boot.jar启动即可。无需安装。


arthas快速入门

  1. 启动arthas。
    在这里插入图片描述
  2. 选择要绑定Java进程,进行绑定。
    在这里插入图片描述

常用指令


dashboard

查看当前系统的实时数据面板,这个面板5s刷新一次。大致数据如下:
在这里插入图片描述
输入q可以退出面板。


thread

这指令类似jstack ,主要是查看当前 JVM 的线程堆栈信息。
在这里插入图片描述
可以通过使用 thread –b 来进行死锁的排查死锁。
参数:

  • -n 指定最忙的前 n 个线程并打印堆栈 。
  • -b 找出阻塞当前线程的线程。
  • -i 指定 cpu 占比统计的采样间隔,单位为毫秒。

jvm

这个就是查看jvm的相关信息。
在这里插入图片描述


jad

反编译指定已加载类的源码。
在这里插入图片描述

trace

使用 trace 命令可以跟踪统计方法耗时。
这里我启用一个spring boot 项目进行演示。
我这里启动了我项目工程的代码,所以路径打了码。
trace 要跟踪类的全限定名 要跟踪的方法名
在这里插入图片描述
现在我通过postmain调用接口
在这里插入图片描述
再回到arthas查看
在这里插入图片描述
我们使用monitor 每 5 秒统计一次上面那个类的mediaSave方法执行情况
monitor -c 时间(单位秒) 要监听的类的全限定名 要监听的方法名
在这里插入图片描述
在这里插入图片描述

watch

观察方法的入参出参信息。
watch 要观察到类全限定名 要观察的方法名 观察表达式
在这里插入图片描述

主要命令汇总

命令介绍
dashboard当前系统的实时数据面板
thread查看当前 JVM 的线程堆栈信息
watch方法执行数据观测
trace方法内部调用路径,并输出方法路径上的每个节点上耗时
stack方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下 调用进行观测
tt方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下 调用进行观测
monitor方法执行监控
jvm查看当前 JVM 信息
vmoption查看,更新 JVM 诊断相关的参数
sc查看 JVM 已加载的类信息
sm查看已加载类的方法信息
jad反编译指定已加载类的源码
classloader查看 classloader 的继承树,urls,类加载信息
heapdump类似 jmap 命令的 heap dump 功能


动态追踪技术

动态追踪技术实际上就是一种不需要重启服务,更改服务代码就能对代码进行问题排查的技术。我们上面介绍的arthas就是用的这种技术,arthas的其中一个核心就是Java Agent。

Java Agent

Java Agent有点类似于我们spring 里的AOP,但它是JVM字节码层面的。
Java Agent的实现有两种方式:

  • premain:这个可以在main方法执行前进行一些操作。
  • agentmain:是控制类运行时的行为(arthas就是用的这种)。
    虽然有两种技术,但是JVM只会执行一种(也就是你只能二选一)。

现在我们来手动实验一下这两种方式:

premain

  1. 引入javassist 用来增强代码(可以理解为改写运行的代码,让代码“变异”)。
    <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.1-GA</version>
    </dependency>
    
  2. 我们写一个要运行的类,这个类将被用来“变异”。
    /**
     * @author Abfeathers
     * @date 2021/3/19
     * @Description VM参数中加入:-javaagent:./agent/target/agent-1.0-SNAPSHOT.jar
     *
     */
    public class MainRun {
        public static void main(String[] args) {
            hello("world");
        }
        private static void hello(String name) {
            System.out.println("hello " + name );
        }
    }
    
    1. 编写agent
      要构建一个 agent 程序,大体可分为以下步骤:

      • 使用字节码增强工具,编写增强代码;

      • 在 manifest 中指定 Premain-Class/Agent-Class 属性;

      • 使用参数加载或者使用 attach 方式改变 app 项目中的内容;
        工程结构如下:
        在这里插入图片描述
        先写一个普通的java类,然后写两个方法premain和agentmain

        import java.lang.instrument.Instrumentation;
        
        /**
         * @author Abfeathers
         * @date 2021/3/19
         * @Description instrument 一共有两个 main 方法,一个是 premain,另一个是 agentmain
         *              但在一个 JVM 中,只会调用一个
         *
         */
        public class AgentApp {
            //在main 执行之前的修改
            public static void premain(String agentOps, Instrumentation inst) {
                System.out.println("==============enter premain==============");
                inst.addTransformer(new Agent());
            }
            //控制类运行时的行为
            public static void agentmain(String agentOps, Instrumentation inst) {
                System.out.println("==============enter agentmain==============");
            }
        }
        

        完成之后,我们就要编写核心了Transformer ,方法增强就得考它了。
        我们编写Agent类,让它实现ClassFileTransformer,然后我们重写Transformer方法,在这个方法里写一些要执行的字节码增强的代码。
        下面代码的逻辑大致是:

        a.获取 MainRun 类的字节码。
        b.获取 hello 方法的字节码。
        c.在方法前后,加入时间统计,首先定义变量 _begin,然后追加要写的代码。
        d.最后把字节码返回。
        
        
        import javassist.ClassPool;
        import javassist.CtClass;
        import javassist.CtMethod;
        
        import java.lang.instrument.ClassFileTransformer;
        import java.lang.instrument.IllegalClassFormatException;
        import java.security.ProtectionDomain;
        
        public class Agent implements ClassFileTransformer {
            @Override
            public byte[] transform(ClassLoader loader, String className,
                                    Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain,
                                    byte[] classfileBuffer) throws IllegalClassFormatException {
                String loadName = className.replaceAll("/", ".");
                //System.out.println(className);
                if (className.endsWith("MainRun")) {
                    try {
                        //javassist 完成字节码增强(打印方法的执行时间<纳秒>)
                        CtClass ctClass = ClassPool.getDefault().get(loadName);
                        CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
                        ctMethod.addLocalVariable("_begin", CtClass.longType);
                        ctMethod.insertBefore("_begin = System.nanoTime();");
                        ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);");
        
                        return ctClass.toBytecode();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return classfileBuffer;
            }
        }
        
    2. 打包agent

      Manifest-Version: 1.0
      Can-Redefine-Classes: true
      Can-Retransform-Classes: true
      premain-class: com.example.javaagent.AgentApp
      agentmain-class: AgentApp
      

      在这里插入图片描述
      在这里插入图片描述
      最后执行指令mvn clean package

    3. 运行MainRun前将参数引入,我这里打包好的agent路径是./agent/target/agent-1.0-SNAPSHOT.jar
      在这里插入图片描述

agentmain

这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:

  • 获取机器上运行的所有 JVM 进程 ID;
  • 选择要诊断的 jvm;
  • 将 jvm 使用 attach 函数链接上;
  • 使用 loadAgent 函数加载 agent,动态修改字节码;
  • 卸载 jvm。

Java Attach API

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。
有了它, 开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Attach API 只有 2 个主要的类,都在 com.sun.tools.attach 包(在 jdk 的 lib 目录下 tools.jar 里面)里面:

VirtualMachine :代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动 作的相反行为,从 JVM 上面解除一个代理)等等 ;
VirtualMachineDescriptor: 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。 Java Attach API 是一个 API 接口,JDK 提供的,它可以将应用程序连接到另一个目标虚拟机。

JVM Attach API 功能上非常简单,主要功能如下 :

  1. Attach 到其中一个 JVM 上,建立通信管道
  2. 让目标 JVM 加载 Agent

了解之后,我们来实验一下:
我们这里直接把所有JVM进程都找到了。
匹配加载了StopWorld.class的JVM进程。
最后打印了一下JDK版本信息。

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Properties;

public class JvmAttach {

    public static void main(String[] args)
            throws Exception {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().endsWith("StopWorld")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                Properties props = virtualMachine.getSystemProperties();
                String version = props.getProperty("java.version");

//                virtualMachine.loadAgent("arthas-boot.jar ","...");
                System.out.println("version:"+version);
                virtualMachine.detach();
            }
        }
    }
}

在这里插入图片描述
其实java agent就类似于安排间谍进行间谍活动,偷偷的在目标进程窃取信息再传输回来。当然真正的动态追踪肯定不是像我这样在目标进程里输出信息,是需要通过IO传输回监控进程的,要不然岂不是跟间谍潜伏后,在敌方一顿宣传我是间谍一样了。


借助 Btrace 手写动态追踪框架

什么是Btrace

btrace已经是个很古老的架构了,它也是一个实现动态追踪技术的架构,已经很少有人用了,因为入门门槛比较高,用起来也麻烦,没有arthas方便。
官方描述是这样的:
BTrace 是基于 Java 语言的一个安全的、可提供动态追踪服务的工具。BTrace 基于 ASM、Java Attach API、Instrument 开发,为用户提供 了很多注解。依靠这些注解,我们可以编写 BTrace 脚本。

配置Btrace

官方git地址
这里我们从官网下载btrace,在本地安装,并配置环境变量。
我这里是mac,我就提供一下我mac的配置。

#配置环境变量
vim ~/.bash_profile

#Btrace
BTRACE_HOME= /Users/abfeathers/btrace-2.1.0-bin
export BTRACE_HOME
export PATH=${PATH}:${BTRACE_HOME}/bin
#使配置文件生效
source ~/.bash_profile

构建一个简单的动态追踪脚手架

  1. 构建一个简单spring boot 工程,其中编写了这么一个类用于动态追踪

    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author Abfeathers
     * @date 2021/3/19 
     * @Description btace动态追踪脚手架测试
     * 
     */
    @RestController
    @RequestMapping("/btrace")
    public class DemoController {
    
        @RequestMapping("/test")
        public String test(@RequestParam("name") String name){
            return "hello,"+name;
        }
    
        @RequestMapping("/exception")
        public String exception(){
            try {
                System.out.println("start.......");
                System.out.println(1/0);
                System.out.println("end.........");
            } catch (Exception e) {
    
            }
            return "success";
        }
    
    }
    
  2. 再构建一个工程引入btrace依赖(也可以通过maven导入)
    在这里插入图片描述

  3. 编写btrace脚本
    @OnMethod 可以指定 clazz 、method、location。

    由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。 拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截。

    1. clazz

      clazz 支持,精准定位、正则表达式定位、 按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如 @OnMethod(clazz="+com.kite.base", method=“doSome”)>、 按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")> method 支持精准定位、正则表达式定位、按注解定位

    2. location

      1. Kind.Entry 与 Kind.Return 分别表示函数的开始和返回,不写 location 的情况下,默认为 Kind.Entry,仅获取参数值,可以用 Kind.Entry ,要获取返回值或执行时 间就要用 Kind.Return

      2. Kind.Error, Kind.Throw 和 Kind.Catch, 表示异常被 throw 、异常被捕获还有异常发生但是没有被捕获的情况,在拦截函数的参数定义里注入一个 Throwable 的参数,代表异 常 3、Kind.Call 表示被监控的方法调用了哪些其他方法,Kind.Line 监测类是否执行到了设置的行数

    package cn.abfeathers.btrace;
    
    import org.openjdk.btrace.core.BTraceUtils;
    import org.openjdk.btrace.core.annotations.*;
    import org.openjdk.btrace.core.types.AnyType;
    
    
    @BTrace
    public class TestBrace {
    
        @OnMethod(
                clazz = "cn.abfeathers.demo.controller.DemoController",
                method = "test",
                location = @Location(Kind.ENTRY)
        )
        public static void checkEntry(
               @ProbeClassName String pcn,
               @ProbeMethodName String pmn,
               AnyType[] args
        ){
            BTraceUtils.println("Class: "+pcn);
            BTraceUtils.println("Method: "+pmn);
            BTraceUtils.printArray(args);
            BTraceUtils.println("===========================");
            BTraceUtils.println();
        }
    
    }
    
    
  4. 使用jps指令查询一下spring boot 进程id
    在这里插入图片描述

  5. 监听进程
    在这里插入图片描述

  6. 访问spring boot
    在这里插入图片描述

  7. 查看监听 在这里插入图片描述
    我们在看另外一个接口的访问,我故意写了除零计算,然后catch了这个异常,正常我们访问是不会出现异常的。
    在这里插入图片描述
    现在我们换个脚本去监听

    package cn.abfeathers.btrace;
    
    import org.openjdk.btrace.core.BTraceUtils;
    import org.openjdk.btrace.core.annotations.*;
    
    /**
     * 类说明:
     */
    @BTrace
    public class TraceException {
        @TLS
        static Throwable currentException;
    
        
        @OnMethod(
                clazz="java.lang.Throwable",
                method="<init>"
        )
        public static void onthrow(@Self Throwable self) {  // @Self其实就是拦截了this
            //new Throwable()
            currentException = self;
        }
    
        @OnMethod(
                clazz="java.lang.Throwable",
                method="<init>"
        )
        public static void onthrow1(@Self Throwable self, String s) {
            //new Throwable(String msg)
            currentException = self;
        }
    
        @OnMethod(
                clazz="java.lang.Throwable",
                method="<init>"
        )
        public static void onthrow1(@Self Throwable self, String s, Throwable cause) {
            //new Throwable(String msg, Throwable cause)
            currentException = self;
        }
    
        @OnMethod(
                clazz="java.lang.Throwable",
                method="<init>"
        )
        public static void onthrow2(@Self Throwable self, Throwable cause) {
            //new Throwable(Throwable cause)
            currentException = self;
        }
    
        
        @OnMethod(
                clazz = "cn.abfeathers.demo.controller.DemoController",
                method = "exception",
                location=@Location(Kind.ERROR)
        )
        public static void onthrowreturn() {
            if (currentException != null) {
                // 打印异常堆栈
                BTraceUtils.Threads.jstack(currentException);
                BTraceUtils.println("=====================");
                // 打印完之后就置空
                currentException = null;
            }
        }
    }
    
    

    我们在访问,发现居然抛错了 ,还在堆栈日志中打印了错误日志在这里插入图片描述
    在这里插入图片描述

BTrace 注解

BTrace 注解可以分为:

  • 类注解 @BTrace
  • 方法注解如@OnMethod
  • 参数注解如:@ProbeClassName
参数注解

@ProbeClassName :用于标记处理方法的参数,仅用户
@OnMethod, 该参数的值就是被跟踪的类名称
@ProbeMethodName用于表示处理方法的参数,仅用户
@OnMethod,该参数值是被跟踪方法名称
@Self当前截取方法的封闭实例参数
@Return当前截取方法的的返回值, 只对location=@Location(Kind.RETURN) 生效
@Duration当前截取方法的执行时间
@TargetInstance 当前截取方法内部调用的实例
@TargetMethodOrField当前截取方法内部被调用的方法名

方法注解

@OnMethod用于指定跟踪方法到目标类,目标方法和目标位置 格式

@Location 属性有:

  • value 默认值为 Kind.ENTRY 即参数的入口位置
  • where 限定探测位置 默认值为 Where.BEFORE 也可以设置为 Where.AFTER
  • clazz
  • method
  • field
  • type
  • line

@Kind 注解的值有

  • Kind.ENTRY-被 trace 方法参数
  • Kind.RETURN-被 trace 方法返回值
  • Kind.THROW -抛异常
  • Kind.ARRAY_SET, Kind.ARRAY_GET -数组索引
  • Kind.CATCH -捕获异常
  • Kind.FIELD_SET -属性值
  • Kind.LINE -行号
  • Kind.NEW -类名
  • Kind.ERROR -抛异常

@OnTimer 用于指定跟踪操作定时执行。value 用于指定时间间隔
@OnError当 trace 代码抛异常或者错误时,该注解的方法会被执行.如果同一个 trace 脚本中其他方法抛异常,该注解方法也会被执行。

Btrace 的限制

BTrace 最终借 Instrument 实现 class 的替换。出于安全考虑,Instrument 在使用上存在诸多的限制,这就好比给一架正在飞行的飞机换 发动机一样一样的,因此 BTrace 脚本的限制如下:

  • 不允许创建对象
  • 不允许创建数组
  • 不允许抛异常
  • 不允许 catch 异常
  • 不允许随意调用其他对象或者类的方法,只允许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输出工具)
  • 不允许改变类的属性
  • 不允许有成员变量和方法,只允许存在 static public void 方法
  • 不允许有内部类、嵌套类
  • 不允许有同步方法和同步块
  • 不允许有循环
  • 不允许随意继承其他类(当然,java.lang.Object 除外)
  • 不允许实现接口 不允许使用 assert 不允许使用 Class 对象

从这么多限制,可以看出BTrace 要做的事:虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。

其实无论是arthas,还是btrace,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。



上一篇:GC调优基础知识工具篇之JDK自带工具

下一篇:JVM调优之内存优化与GC优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值