java虚拟机11

动态追踪技术底层分析

什么是动态追踪

  • 不用关闭java程序重启,无侵入式的实现,即可统计java程序的运行处理信息

  • 通过java agent技术实现

Java Agent 技术

  • JVM级别的aop

    • 事前、事后、事中
  • 比如要打印方法的入参和出参,此时是需要对java代码进行修改的,但是java程序已经运行了,数据就在运行时数据区中,而class文件就在方法区中,如果要改变某一个方法,就需要替换class文件,修改相应的字节码

  • 一个JVM只能调用一个arthas

main方法
  • premain方法
  • agentmain方法,arthas就是使用的这种

premain实例

agent的实际项目
  • package com.example.javaagent.app;
    
    //VM参数中加入:-javaagent:F:\work_vip\javaagent-demo\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 );
            try {
                Thread.sleep(Integer.MAX_VALUE);//线程休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
agent构建步骤
  • 1.编写agent

    • AgentApp类中包含premain方法和agentmain方法

    • package com.example.javaagent;
      
      import java.lang.instrument.Instrumentation;
      /**
       * instrument 一共有两个 main 方法,一个是 premain,另一个是 agentmain
       *  但在一个 JVM 中,只会调用一个
       */
      
      public class AgentApp {
          //在main 执行之前的修改
          public static void premain(String agentOps, Instrumentation inst) {
              System.out.println("==============enter premain==============");
              //System.out.println(agentOps);
              inst.addTransformer(new Agent());
          }
          //控制类运行时的行为
          public static void agentmain(String agentOps, Instrumentation inst) {
              System.out.println("==============enter agentmain==============");
          }
      }
      
      
  • 2.编写transformer

    • package com.example.javaagent;
      
      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;
          }
      }
      
      
    • 去实现一个class文件变异的接口

    • 对MainRun这个类进行变异,同时对hello方法进行变异,增加了一个局部变量-----当前系统时间,并在方法执行前插入,同时在方法结束后打印这个参数和当前时间的差值,也就是方法的执行时间

  • 3.打包

    • 在\resources\META-INF目录下,新建一个MANIFEST.MF文件

      Manifest-Version: 1.0
      Can-Redefine-Classes: true
      Can-Retransform-Classes: true
      premain-class: com.example.javaagent.AgentApp
      agentmain-class: AgentApp
      
    • 在maven配置中增加,避免在idea自带maven打包时mf文件被替换

      <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-jar-plugin</artifactId>
                      <configuration>
                          <archive>
                              <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                          </archive>
                      </configuration>
                  </plugin>
              </plugins>
          </build>
      
    • 然后在agent项目里执行mvn install

Agentmain实例

  • 这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:
    • 获取机器上运行的所有 JVM 进程 ID;
    • 选择要诊断的 jvm,选择进程id
    • 将 jvm 使用 attach 函数链接上;
    • 使用 loadAgent 函数加载 agent,动态修改字节码;
    • 卸载 jvm。
  • 这种模式不同于premain方法,premain方法所有的干预都会显示到被监控的程序里面,而agentmain则不会,它不会影响监控程序的输出,而是会将坚决信息回传回agentmain的监控程序,并显示在监控程序里面,arthas就是如此,例如使用arthas的watch命令,最终的监控结果会在arthas自己的命令行界面显示
Java Attach API
实现方法1
  • package ex10.attach;
    
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    
    import java.util.List;
    import java.util.Properties;
    
    //Java Attach API
    public class AttachDemo {
        public static void main(String[] args) throws Exception {
            //VM进程号,通过 jps命令获取
            //attach向目标 JVM ”附着”(Attach)代理工具程序
            VirtualMachine vm = VirtualMachine.attach("8900");
            // get system properties in target VM
            Properties props = vm.getSystemProperties();
            String version = props.getProperty("java.version");
            System.out.println(version);
            // VirtualMachineDescriptor是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
            List<VirtualMachineDescriptor> vmDescriptors = vm.list();
            //从JVM上面解除代理
            vm.detach();
        }
    }
    
    
  • 注意,使用上述代码,需要引入E:\Java\JDK\lib\tools.jar这个jar包

实现方法2
  • package ex10.attach;
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    
    import java.util.List;
    import java.util.Properties;
    import java.util.Set;
    
    /**
     * @author King老师
     * Attach使用入门
     */
    
    public class JvmAttach {
    
        public static void main(String[] args)
                throws Exception {
            List<VirtualMachineDescriptor> list = VirtualMachine.list();
            for (VirtualMachineDescriptor vmd : list) {
                //只找对应启动类是JVMObject结尾的
                if (vmd.displayName().endsWith("JVMObject")) {
                    VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                    Properties props = virtualMachine.getSystemProperties();
                    //打印attach上的VM所有的系统属性
                    System.out.println(getChildProcessConfig(props));
                    //打印attach上的VM的JDK版本信息
                    String version = props.getProperty("java.version");
                    System.out.println("----version:"+version);
                    virtualMachine.detach();
                }
            }
        }
        //获取所有属性
        private static Properties getChildProcessConfig( Properties props) {
            Properties properties = System.getProperties();
            Set<String> stringPropertyNames = properties.stringPropertyNames();
            Properties prop = new Properties();
            for (String string : stringPropertyNames) {
                prop.setProperty(string, properties.getProperty(string));
            }
            return prop;
        }
    
    }
    
    
    
Instrument

实战案例

  • package com.example.javaagent.app;
    
    
    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("MainRun")) {
                    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();
                }
            }
        }
    }
    
    
    
  • 获取对应方法的jvm,并加载对应的代理jar包,然后去修改字节码

  • 这里是利用arthas实现的,例如arthas的watch方法,自己会把监视的结果回传回来,这是底层实现的,代码隐藏了,但是本质就是通过网络通讯回传回来的

总结

  • 上述只模拟了一个premain的方法,agentmain的模式没有模拟,只是以springboot项目为基础,拿arthas演示了一下watch方法,上面的实战实例和java attach api内容可以不自己看,如果想仔细看,去看第二期的内容,而模拟的springboot项目的部分内容如下

    • package cn.enjoyedu.demo.controller;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * 类说明:
       */
      @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";
          }
      
      }
      
      

借助Btrace手写动态追踪框架

  • 出现了非常久了,但是对技术要求很高,而arthas使用门槛很低

  • 在arthas诞生之前,都是通过Btrace实现追踪

  • btrace的github地址:

    • https://github.com/btraceio/btrace,
      BTrace基于ASM、Java Attach API、Instrument开发
    • ASM是字节码增强工具,但是接口比较难懂,像CGLIB就是基于ASM实现的,ASM可以在JVM运行过程中动态创建一个class文件

实战演练

  • 下载Btrace,并配置环境变量(类似jdk)

  • 命令行输入btrace,显示内容则成功

  • 1.仍然以springboot哪个hello,test项目为例

    • DemoController

      package cn.enjoyedu.demo.controller;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * 类说明:
       */
      @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的三个类

    • TestBrace

      package cn.enjoyedu.btrace;
      
      import com.sun.btrace.AnyType;
      import com.sun.btrace.BTraceUtils;
      import com.sun.btrace.annotations.*;
      
      import static com.sun.btrace.BTraceUtils.str;
      import static com.sun.btrace.BTraceUtils.strcat;
      
      /**
       * 类说明:检查方法的输入和输出
       */
      @BTrace
      public class TestBrace {
      
          // 跟踪的方法
          @OnMethod(
                  clazz = "cn.enjoyedu.demo.controller.DemoController",
                  method = "test",
                  location = @Location(Kind.ENTRY)
          )
          public static void checkEntry(
                 @ProbeClassName String pcn,
                 @ProbeMethodName String pmn,
                 AnyType[] args
          ){
              //打印了方法的参数
              //BTraceUtils这个类就具有远程打印功能,就是在BTrace本身打印,而不是在springboot那个项目里面打印
              BTraceUtils.println("Class: "+pcn);
              BTraceUtils.println("Method: "+pmn);
              BTraceUtils.printArray(args);
              BTraceUtils.println("===========================");
              BTraceUtils.println();
          }
      
          @OnMethod(
                  clazz = "cn.enjoyedu.demo.Service.NormalService",
                  method = "getBoolean",/*这里需要修改*/
                  location = @Location(Kind.RETURN)/*这里需要修改*/
          )
          public static void checkReturn(/*这里需要修改*/
                 @ProbeClassName String pcn,
                 @ProbeMethodName String pmn,
                 @Return boolean result /*这里需要修改*/
          ){
              BTraceUtils.println("Class: "+pcn);
              BTraceUtils.println("Method: "+pmn);
              BTraceUtils.println(strcat("result:",str(result)));/*这里需要修改*/
              BTraceUtils.println("===========================");
              BTraceUtils.println();
          }
      }
      
      
    • MoreBtrace

      package cn.enjoyedu.btrace;
      
      import com.sun.btrace.BTraceUtils;
      import com.sun.btrace.annotations.*;
      
      import static com.sun.btrace.BTraceUtils.*;
      
      /**
       * 类说明:
       */
      @BTrace
      public class MoreBtrace {
      
          // 在testBtrace的实例中加入很多的功能
          // 注解写的更复杂了
          // Location是动作,方法调用
          // clazz、method是正则规则
          // where是after之后
          @OnMethod(
                  clazz = "cn.enjoyedu.demo.controller.DemoController",
                  method = "test",
                  location = @Location(value = Kind.CALL,
                          clazz = "/.*/", method = "/.*/",
                          where = Where.AFTER))
          public static void onInvoke(@Self Object self, @TargetInstance Object instance,
                                    @TargetMethodOrField String method,
                                    @Duration long duration){
              BTraceUtils.println(strcat("self: ", str(self)));
              BTraceUtils.println(strcat("instance: ", str(instance)));
              BTraceUtils.println(strcat("method: ", str(method)));
              BTraceUtils.println(strcat("duration(ns): ", str(duration )));
              println("===========================");
              BTraceUtils.println();
          }
      
          @OnMethod(
                  clazz = "cn.enjoyedu.demo.controller.DemoController",
                  location = @Location(value = Kind.LINE, line = 26))
          public static void onBind() {
              println("execute line 20");
              println("---------------------------");
              BTraceUtils.println();
          }
      
          @OnMethod(
                  clazz = "/cn\\.enjoyedu\\.demo\\.controller\\..*/",
                  method = "/.*/",
                  location = @Location(Kind.RETURN))
          public static void slowQuery(@ProbeClassName String pcn,
                                       @ProbeMethodName String probeMethod,
                                       @Duration long duration){
              if(duration > 1000000 * 100){
                  println(strcat("class:", pcn));
                  println(strcat("method:", probeMethod));
                  println(strcat("duration:", str(duration / 1000000)));
                  println("*************************");
                  BTraceUtils.println();
              }
      
          }
      }
      
      
    • TraceException

      package cn.enjoyedu.btrace;
      
      import com.sun.btrace.BTraceUtils;
      import com.sun.btrace.annotations.*;
      
      /**
       * 类说明:
       */
      @BTrace
      public class TraceException {
          @TLS
          static Throwable currentException;
      
          // introduce probe into every constructor of java.lang.Throwable
          // class and store "this" in the thread local variable.
          @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;
          }
      
          // when any constructor of java.lang.Throwable returns
          // print the currentException's stack trace.
          @OnMethod(
                  clazz = "cn.enjoyedu.demo.controller.DemoController",
                  method = "exception",
                  location=@Location(Kind.ERROR)
          )
          public static void onthrowreturn() {
              if (currentException != null) {
                  // 打印异常堆栈
                  BTraceUtils.Threads.jstack(currentException);
                  BTraceUtils.println("=====================");
                  // 打印完之后就置空
                  currentException = null;
              }
          }
      }
      
      

      location=@Location(Kind.ERROR)可以把吞掉的异常打印出来

  • 3.具体的监控步骤

    • 进入TestBrace所在的命令行界面,然后输入:

      btrace 监控的程序的进程号 TestBrace.java

    • 此时在浏览器中访问springboot项目中的test方法,输入king,然后就能在Btrace的命令行界面看到相应的输出

注解的使用

OnMethod
  • @OnMethod 可以指定 clazz 、method、location。
  • 由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截
clazz
  • clazz 支持,精准定位、正则表达式定位、按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如@OnMethod(clazz="+com.kite.base", method=“doSome”)>、按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService",method="@javax.jws.WebMethod")>method 支持精准定位、正则表达式定位、按注解定位
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 监测类是否执行到了设置的行数

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 要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。

工具总结

  • 其实作为 Java 的动态追踪技术,站在比较底层的角度上来说,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。Arthas 都是针前面这些技术的一个封装而已。
  • Btrace 功能虽然强大,但都是比较难入门,这就是为什么 Btrace 出来这么多年,还是只在小范围内被使用。相对来说,Arthas 显的友好而且安全的多。
  • 但无论工具如何强大,一些基础知识是需要牢固掌握的,否则,工具中出现的那些术语,也会让人一头雾水。工具常变,但基础更加重要。如果你想要一个适应性更强的技术栈,还是要多花点时间在原始的排查方法上。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值