Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

前言

在深入到 Arthas 的原理之前我们先看一个有趣的示例,我们依然使用前面文章中用到的代码示例,

public class DemoApplication {

 public static void main(String[] args) {
  for (int i = 0; i < Integer.MAX_VALUE; i++) {
   System.out.println("times:" + i + " , result:" + testExceptionTrunc());
  }
 }

 public static boolean testExceptionTrunc()  {
  try {
   // 人工构造异常抛出的场景
   ((Object)null).getClass();
  } catch (Exception e) {
   if (e.getStackTrace().length == 0) {
    try {
     // 堆栈消失的时候当前线程休眠5秒,便于观察
     Thread.sleep(5000);
    } catch (InterruptedException interruptedException) {
     // do nothing
    }
    return true;
   }
  }
  return false;
 }
}

运行这段代码,然后我们使用 Arthas 的tt命令记录testExceptionTrunc的每次调用的情况

tt -t com.IDEAlism.demo.DemoApplication testExceptionTrunc

再新开一个窗口,打开 Arthas,使用dump命令把正在运行的字节码输出到本地文件后查看此时的字节码Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

可以看到,现在正在运行的字节码和我们从源码编译过来的相比多了两行,多的这两行正是 Arthas 插装的代码,Arthas 的一切魔法都从这里开始。

实现一个极简的 watch 命令

给运行中的代码插装新的代码片段,这个特性 JVM 从 SE6 就已经开始支持了,所有有关代码插装的 API 都在Java.lang.instrument.Instrumentation这个包中。有了 JVM 的支持和 Arthas 的启发,我们可以借助代码插装实现一个极简版的watch命令,这样的一个小工具有以下特点:

  • 可以统计被插装方法的运行时间
  • 被插装的代码不感知该工具的存在,该工具动态 attach 到目标类的 JVM 中
  • 为了示例简单明了,只实现计时功能,不实现watch的其他功能

由于插装代码是一件过于底层且需要对字节码有很高的掌握度,所以我们引入了一个二方包javassist来做具体的插装工作,maven 坐标如下:

<dependency>
   <groupId>org.javassist</groupId>
   <artifactId>javassist</artifactId>
   <version>3.21.0-GA</version>
</dependency>

我们会使用 javassist 最基础的功能,详细的使用教程请参考https://www.baeldung.com/javassist

我们的目标类是我们前面文章中一直使用的 Demo 类,代码如下:

public class DemoApplication {

 private static Logger LOGGER = LoggerFactory.getLogger(DemoApplication.class);

 public static void main(String[] args) {
  for (int i = 0; i < Integer.MAX_VALUE; i++) {
//   System.out.println("times:" + i + " , result:" + testExceptionTrunc());
   testExceptionTruncate();
  }
 }

 public static void testExceptionTruncate()  {
  try {
   // 人工构造异常抛出的场景
   ((Object)null).getClass();
  } catch (Exception e) {
   if (e.getStackTrace().length == 0) {
    System.out.println("stack miss;");
    try {
     // 堆栈消失的时候当前线程休眠5秒,便于观察
     Thread.sleep(5000);
    } catch (InterruptedException interruptedException) {
     // do nothing
    }
   }
  }
  System.out.println("stack still exist;");
 }
}

为了方便插装代码打印日志,我们引入了一个静态的LOGGER,并且将testExceptionTruncate改为返回void类型的返回值,这样的改动让代码插装更加简单。

如何让两个运行中的 JVM 建立连接呢,JVM 通过attach api支持了这种场景

public static void run(String[] args) {
  String agentFilePath = "/Users/jnzh/Documents/Idea Project/agent/out/agent.jar";
  String applicationName = "com.idealism.demo.DemoApplication";

  //iterate all jvms and get the first one that matches our application name
  Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
    .stream()
    .filter(jvm -> {
     LOGGER.info("jvm:{}", jvm.displayName());
     return jvm.displayName().contains(applicationName);
    })
    .findFirst().get().id());

  if(!jvmProcessOpt.isPresent()) {
   LOGGER.error("Target Application not found");
   return;
  }
  File agentFile = new File(agentFilePath);
  try {
   String jvmPid = jvmProcessOpt.get();
   LOGGER.info("Attaching to target JVM with PID: " + jvmPid);
   VirtualMachine jvm = VirtualMachine.attach(jvmPid);
   jvm.loadAgent(agentFile.getAbsolutePath());
   jvm.detach();
   LOGGER.info("Attached to target JVM and loaded Java agent successfully");
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }

运行这个方法的 JVM 通过名称匹配目标 JVM,然后通过attach方法与目标 JVM 取得联系,继而对目标 JVM 发出指令,让其挂载插装agent,整个过程如下图所示:

Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

在我们反复提的 agent 里,我们才真正做代码插装的工作,attach API中要求,被目标代码挂载的agent包必须实现agentmain 且在打包的 MANIFEST.MF 中指定 Agent-Class 属性,完整的 MANIFEST.MF 文件如下所示:

Manifest-Version: 1.0
Main-Class: com.idealism.agent.AgentApplication
Agent-Class: com.idealism.agent.AgentApplication
Can-Redefine-Classes: true
Can-Retransform-Classes: true

有个小插曲,用 IDEA 打 JAR 包的时候,指定的 MANIFEST.MF 的路径到${ProjectName}/src就可以,默认的需要删掉框中的路径,否则,打出来的 MANIFEST.MF 文件不会生效。Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

agentmain方法中我们实现了对目标类的插装,目标 JVM 在被attach后会自动调用这个方法:

public static void agentmain(String agentArgs, Instrumentation inst) {
  LOGGER.info("[Agent] In agentmain method");

  String className = "com.idealism.demo.DemoApplication";
  transformClass(className,inst);
 }

transformClass方法做了一层转发:

private static void transformClass(String className, Instrumentation instrumentation) {
  Class<?> targetCls = null;
  ClassLoader targetClassLoader = null;
  // see if we can get the class using forName
  try {
   targetCls = Class.forName(className);
   targetClassLoader = targetCls.getClassLoader();
   transform(targetCls, targetClassLoader, instrumentation);
   return;
  } catch (Exception ex) {
   LOGGER.error("Class [{}] not found with Class.forName");
  }
  // otherwise iterate all loaded classes and find what we want
  for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
   if(clazz.getName().equals(className)) {
    targetCls = clazz;
    targetClassLoader = targetCls.getClassLoader();
    transform(targetCls, targetClassLoader, instrumentation);
    return;
   }
  }
  throw new RuntimeException("Failed to find class [" + className + "]");
 }

最后的流程会调用方法:

private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
  TimeWatcherTransformer dt = new TimeWatcherTransformer(clazz.getName(), classLoader);
  instrumentation.addTransformer(dt, true);
  try {
   instrumentation.retransformClasses(clazz);
  } catch (Exception ex) {
   throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
  }
 }

在这里我们实现了一个TimeWatcherTransformer并将代码插装的工作委托给它来做:

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] byteCode = classfileBuffer;

        String finalTargetClassName = this.targetClassName.replaceAll("\.""/"); //replace . with /
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            LOGGER.info("[Agent] Transforming class DemoApplication");
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(TEST_METHOD);
                m.addLocalVariable("startTime", CtClass.longType);
                m.insertBefore("startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append("endTime = System.currentTimeMillis();");
                endBlock.append("opTime = (endTime-startTime)/1000;");

                endBlock.append("LOGGER.info("[Application] testExceptionTruncate completed in:" + opTime + " seconds!");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }
        return byteCode;
    }

有了JVM的支持,我们实现一个简单的watch命令也不难,只需要在目标方法的前后插入时间语句就可以了,目标JVM在attach了我们的agent后会输出本次调用的时间,如下图所示:

Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

JVM Attach 机制的实现

在前面的例子里我们之所以可以在一个 JVM 中发送指令让另一个 JVM 加载 Agent,是因为 JVM 通过 Attach 机制提供了一种进程间通信的方式,http://lovestblog.cn/blog/2014/06/18/jvm-attach/?spm=ata.13261165.0.0.26d52428n8NoAy 详细的讲述了 Attach 机制是如何在 Linux 平台下实现的,结合我们之前的例子,可以把整个过程总结为如下的一张图:

Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

在 GVM 调用attach的时候如果发现没有java_pid这个文件,则开始启动attach机制,首先会创建一个attach_pid的文件,这个文件的主要作用是用来鉴权。然后向Signal Dispacher发送BREAK信号,之后就一直在轮询等待java_pid这个套接字。Signal Dispacher中注册的信号处理器Attach Listener中首先会校验attach_pid这个文件的 uid 是否和当前 uid 一致,鉴权通过后才会创建attach_pid建立通信通道。

正文

– END –
历史文章

OGNL语法规范
消失的堆栈


扫码关注我

Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

公众号|程序员的理想主义




原文始发于微信公众号(苦味代码):Arthas原理系列(一):利用JVM的attach机制实现一个极简的watch命令

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21611.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!