• 【转】动态字节码技术跟踪Java程序


     Whats is Java Agent?   .. java.lang.instrument.Instrumentation

     

    之前有写 基于AOP的日志调试 讨论一种跟踪Java程序的方法, 但不是很完美.后来发现了 Btrace , 由于它借助动态字节码注入技术 , 实现优雅且功能强大.
    只不过, 用起来总是磕磕绊绊的, 时常为了跟踪某个问题, 却花了大把的时间调试Btrace的脚本. 为此, 我尝试将几种跟踪模式固化成脚本模板, 待用的时候去调整一下正则表达式之类的.
    跟踪过程往往是假设与验证的螺旋迭代过程, 反复的用BTrace跟踪目标进程, 总有那么几次莫名其妙的不可用, 最后不得不重启目标进程. 若真是线上不能停的服务, 我想这种方式还是不靠谱啊.
    为此, 据决定自己的搞个用起来简单, 又能良好支持反复跟踪而不用重启目标进程的工具.

    AOP

    AOP是Btrace, jip1等众多监测工具的核心思想, 用一段代码最容易说明:

    1
    2
    3
    4
    5
    public void say(String words){
      Trace.enter();
      System.out.println(words);
      Trace.exit();
    }

    如上, Trace.enter() 和 Trace.exit() 将say(words)内的代码环抱起来, 对方法进出的进行切面的处理, 便可获取运行时的上下文, 如:

    • 调用栈
    • 当前线程
    • 时间消耗
    • 参数与返回值
    • 当前实例状态

    实现的选择

    实现切面的方式, 我知道的有以下几种:

    代理(装饰器)模式

    设计模式中装饰器模式和代理模式, 尽管解决的问题域不同, 代码实现是非常相似, 均可以实现切面处理, 这里视为等价. 依旧用代码说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    interface Person {
      void say(String words);
    }
     
    class Officer implements Person {
      public void say(String words) { lie(words); }
      private void lie(String words) {...}
    }
     
    class Proxy implements Person {
      private final Officer officer;
      public Proxy(Officer officer) { this.officer = officer; }
      public void say(String words) {
        enter();
        officer.say(words);
        exit();
      }
      private void enter() { ... }
      private void exit() { ... }
    }
     
    Person p = new Proxy(new Officer());

    很明显, 上述enter() 和exit()是实现切面的地方, 通过获取Officer的Proxy实例, 便可对Officer实例的行为进行跟踪. 这种方式实现起来最简单, 也最直接.

    Java Proxy

    Java Proxy是JDK内置的代理API, 借助反射机制实现. 用它来是完成切面则会是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class ProxyInvocationHandler implements InvocationHandler {
      private final Object target;
      public ProxyInvocationHandler(Object target) { this.target = target;}
      public Object handle(Object proxy, Method method, Object[] args) {
        enter();
        method.invoke(target, args);
        exit();
      }
      private void enter() { ... }
      private void exit() { ... }
    }
    ClassLoader loader = ...
    Class<?>[]  interfaces = {Person.class};
    Person p = (Person)Proxy.newInstance(loader, interfaces, new ProxyInvocationHandler(new Officer()));

    相比较上一中方法, 这种不太易读, 但更为通用, 对具体实现依赖很少.

    AspectJ

    AspectJ是基于字节码操作(运行时利用ASM库)的AOP实现, 相比较Java proxy, 它会显得对调用更”透明”, 编写更简明(类似DSL), 性能更好. 如下代码:

    1
    2
    3
    pointcut say(): execute(* say(..))
    before(): say() { ... }
    after() : say() { ... }

    Aspectj实现切面的时机有两种: 静态编译和类加载期编织(load-time weaving). 并且它对IDE的支持很丰富.

    CGlib

    与AspectJ一样CGlib也是操作字节码来实现AOP的, 使用上与Java Proxy非常相似, 只是不像Java Proxy对接口有依赖, 我们熟知的Spring, Guice之类的IoC容器实现AOP都是使用它来完成的.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Callback implements MethodInterceptor {
      public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
        enter();
        proxy.invokeSuper(obj, args);
        exit();
      }
      private void enter() { ... }
      private void exit() { ... }
    }
    Enhancer e = new Enhancer();
    e.setSuperclass(Officer.class);
    e.setCallback(new Callback());
    Person p = e.create();

    字节码操纵

    上面四种方法各有适用的场景, 但唯独对运行着的Java进程进行动态的跟踪支持不了, 当然也许是我了解的不够深入, 若有基于上述方案的办法还请不吝赐教.

    还是回到Btrace的思路上来, 在理解了它借助java.lang.Instrumentation进行字节码注入的实现原理后, 实现动态变化跟踪方式或目标应该没有问题.

    借下来的问题, 如何操作(注入)字节码实现切面的处理. 可喜的是, “构建自己的监测工具”一文给我提供了一个很好的切入点. 在此基础上, 经过一些对ASM的深入研究, 可以实现:

    • 方法调用进入时, 获取当前实例(this) 和 参数值列表;
    • 方法调用出去时, 获取返回值;
    • 方法异常抛出时, 触发回调并获取异常实例.

    其切面实现的核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    private static class ProbeMethodAdapter extends AdviceAdapter {
     
        protected ProbeMethodAdapter(MethodVisitor mv, int access, String name, String desc, String className) {
          super(mv, access, name, desc);
          start = new Label();
          end = new Label();
          methodName = name;
          this.className = className;
        }
     
        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
          mark(end);
          catchException(start, end, Type.getType(Throwable.class));
          dup();
          push(className);
          push(methodName);
          push(methodDesc);
          loadThis();
          invokeStatic(Probe.TYPE, Probe.EXIT);
          visitInsn(ATHROW);
          super.visitMaxs(maxStack, maxLocals);
        }
     
        @Override
        protected void onMethodEnter() {
          push(className);
          push(methodName);
          push(methodDesc);
          loadThis();
          loadArgArray();
          invokeStatic(Probe.TYPE, Probe.ENTRY);
          mark(start);
        }
     
        @Override
        protected void onMethodExit(int opcode) {
          if (opcode == ATHROW) return; // do nothing, @see visitMax
          prepareResultBy(opcode);
          push(className);
          push(methodName);
          push(methodDesc);
          loadThis();
          invokeStatic(Probe.TYPE, Probe.EXIT);
        }
     
        private void prepareResultBy(int opcode) {
          if (opcode == RETURN) { // void
            push((Type) null);
          } else if (opcode == ARETURN) { // object
            dup();
          } else {
            if (opcode == LRETURN || opcode == DRETURN) { // long or double
              dup2();
            } else {
              dup();
            }
            box(Type.getReturnType(methodDesc));
          }
        }
     
        private final String className;
        private final String methodName;
        private final Label start;
        private final Label end;
    }

    更多参考请见这里的 Demo , 它是javaagent, 在伴随宿主进程启动后, 提供MBean可用jconsole进行动态跟踪的管理.

    后续的方向

    1. 提供基于Web的远程交互界面;
    2. 提供基于Shell的本地命令行接口;
    3. 提供Profile统计和趋势输出;
    4. 提供跟踪日志定位与分析.

    参考

    1. The Java Interactive Profiler
    2. Proxy Javadoc
    3. Aspectj
    4. CGlib
    5. BTrace User’s Guide
    6. java动态跟踪分析工具BTrace实现原理
    7. 构建自己的监测工具
    8. ASM Guide
    9. 常用 Java Profiling 工具的分析与比较
    10. AOP@Work: Performance monitoring with AspectJ
    11. The JavaTM Virtual Machine Specification
    12. 来自rednaxelafx的JVM分享, 他的 Blog
    13. BCEL
    京东技术
  • 相关阅读:
    正则表达式(验证账号密码邮箱身份证)
    JS Fetch
    事件流动
    JS DOM和BOM
    CSS的定位
    For each...in / For...in / For...of 的解释和例子
    CSS的gridlayout
    CSS position属性
    CSS的颜色
    twelfth week
  • 原文地址:https://www.cnblogs.com/wely/p/6198696.html
Copyright © 2020-2023  润新知