深入剖析arthas技术原理

写在文章开头

笔者一直强调,计算机是一门理性的学科,一切都是需要可量化和落地的,而本文的arthas是一款强大的监控诊断工具,了解其设计理念和工作机制,可以辅助我们更好的使用这款工具。而本文将从jdk8版本兼容的旧有版本,即arhtas 3.6.0的源码角度出发,来深入分析和学习arthas出色的设计理念,希望对你日常使用有所帮助。

你好,我是 SharkChili ,禅与计算机程序设计艺术布道者,希望我的理念对您有所启发。

📝 我的公众号:写代码的SharkChili

在这里,我会分享技术干货、编程思考与开源项目实践。

🚀 我的开源项目:mini-redis

一个用于教学理解的 Redis 精简实现,欢迎 Star & Contribute:
github.com/shark-ctrl/...

👥 欢迎加入读者群

关注公众号,回复 【加群】 即可获取联系方式,期待与你交流技术、共同成长!

前置知识铺垫

环境说明

考虑到大部分读者使用的jdk版本为jdk8,所以笔者选择artahs 3.6.0的源码来展开演示本文的所有内容,无论是macOS还是Windows用户,请严格遵循如下几个环境说明:

  1. 克隆或者下载arhtas-3.6.0版本:github.com/alibaba/art...
  1. 调试时,程序会从本地拉取arthas-core和agent文件,请读者提前完成arhtas-3.6.0 zip包的下载:

并解压到默认读取路径,以mac os为例则是:

bash 复制代码
/Users/用户名/.arthas/lib/3.6.0/arthas/arthas-agent.jar和 arthas-core.jar

对应Windows用户则是:

bash 复制代码
C:/Users/用户名/.arthas/lib/3.6.0/arthas/arthas-agent.jar和 arthas-core.jar

jvm如何工作的

在此之前,我们还是需要了解一下java程序的执行过程,方便对后文的理解,按照周志明老师《深入理解jvm虚拟机》说法,对应java程序分为前端编译和后端编译,这里前端编译我们可以理解为程序运行前的操作,该阶段会将写好的java代码通过javac编译器编译为虚拟机所能理解的字节码。

基于上述的处理构建成字节码也就是程序语言的中间表示形式,我们键入java指令启动程序,此时就会通过C++启动一个虚拟机实例处理上述字节码,也就是将字节码转为本地基础设施也就是操作系统所能识别的指令集并运行,这也就是后端编译,即: 解释器不断解释生成操作系统可理解的机器码运行 对于一些热点代码,虚拟机JIT会将其优化并编译为机器码缓存,后续则以机器码版本运行。基于上述步骤虚拟机以方法作为基本单元,即以栈帧来支持方法的调用和这些指令方法对应的数据结构的执行:

arhtas增强工作机制

因为java是一门半截式半编译的语言,所以在运行时就有了很大的可操作空间,例如我们日常的web接口有大量的业务查询逻辑,就像下面这段代码:

typescript 复制代码
public class UserService {

    public JSONObject findById(long id) {
        //模拟查询和返回用户数据
        ThreadUtil.sleep(RandomUtil.randomInt(200));
        JSONObject jsonObject = new JSONObject().putOnce("id", id).putOnce("name", "张三");
        System.out.println("查询结果:"+jsonObject);
        return jsonObject;
    }
}

此时我们为了统一打印输出接口耗时,强行在既有代码中进行各种硬编码不仅非常繁琐,还会让研发人员过分侵入到一些非业务工作以外的事情,影响编码的效率。

所以设计者基于java程序运行的特性在jdk5中引入了一种字节码增强的技术,其底层依赖于JVMTI也就是JVM Tool interface,它是JVM暴露出来来用户扩展的接口集合,基于这些接口编写我们就可以拓展开发属于自己的增强逻辑:

该技术范围启动前增强和启动时增强,启动前增强顾名思义则是JVM启动时针对字节码做的增强操作,对应我们可以通过实现如下方法,基于入参instrumentation编写增强逻辑,例如arthas-agentAgentBootstrap就基于该函数实现了启动前增强逻辑:

typescript 复制代码
 public static void premain(String args, Instrumentation inst) {
        main(args, inst);//字节码增强逻辑
    }

然后在打jar包时,通过Premain-Class指定入口类即可:

vbnet 复制代码
<Premain-Class>com.taobao.arthas.agent334.AgentBootstrap</Premain-Class>

对应我们也给出运行流程图,如下图,我们通过-jar 参数将打包好的jar包attach到目标虚拟机,在启动前通过addTransformer将字节码转换器添加到目标JVM拦截所有类实现定制化字节码增强。

此时,对应JVM进行类加载时,就会通过ClassFileTransformer完成字节码修改实现运行前增强,这种做法最典型的运用场景便是日常的idea破解即通过-javaagent:/xxxx.jar=param指定jar包完成运行前增强修改加密文件和证书:

而另外一种则是运行时增强,最典型的运用就是artahs,运行时增强的实现则是通过实现agentmain函数,对应我们也可以在arthas-agent看到这个函数的实现:

typescript 复制代码
 public static void agentmain(String args, Instrumentation inst) {
         main(args, inst);
    }

agentmain完成增强逻辑后,针对打包的jar通过Agent-Class指定入口类:

vbnet 复制代码
 <Agent-Class>com.taobao.arthas.agent334.AgentBootstrap</Agent-Class>

对应其工作流程如下图,通过VirtualMachine遍历定位到目标进程后,将agent attach到目标进程,执行agentmain完成jvm实例增强:

arthas对于字节码增强的应用

arthas就是利用运行时增强的典型运用,其本质上就是在运行时通过attach到目标虚拟机,针对匹配的类进行通过高效简洁字节码工具bytekit完成字节码增强,而这也就是arthas的monitor、stack、trace等命令实现的本质。

对此笔者也给出一个简单的bytekit示例,我们还是以上面的UserService为例,我们希望针对这个类进行耗时打印同时耗时的逻辑不侵入到业务代码中,对此我们就可以用到阿里的bytekit这款简单易上手的字节码增强工具完成接口耗时打印的字节码增强。

首先我们引入相关的依赖,对应依赖和版本如下

xml 复制代码
<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>bytekit-core</artifactId>
            <version>0.1.5</version>
        </dependency>

        <dependency>
            <groupId>org.benf</groupId>
            <artifactId>cfr</artifactId>
            <version>0.152</version>
        </dependency>


        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.14.9</version>
        </dependency>

然后我们就可以落地下面这样一段代码,即基于bytekit注解声明方法执行前后的增强逻辑,以笔者本次的案例为例,本质就是:

  1. 通过atEnter注解声明拦截逻辑执行前的逻辑,即记录开始时间,并通过inline指明逻辑直接放在方法内部,这种做法的好处是在于jit进行逃逸分析时可以做一些类似于方法内联等优化执行效率的逻辑
  2. atEit声明方法执行的后置逻辑,即打印输出耗时

对应的拦截器的逻辑如下:

csharp 复制代码
public class SampleInterceptor  {
    
    public static long start;

    @AtEnter(inline = true)
    public static void atEnter() {
        //记录开始时间
        start = System.currentTimeMillis();
    }

    @AtExit(inline = true)
    public static void atEit() {
        //输出打印被增强的方法总耗时
        System.out.println("cost:" + (System.currentTimeMillis() - start));
    }
}

随后我们再给出对应的增强逻辑,核心代码如下:

  1. 解析上述SampleInterceptor注解
  2. 获取UserService字节码
  3. 定位到findById,通过解析后得到的processors对该字节码进行增强
  4. 打印字节码和增强后的逻辑
ini 复制代码
public static void main(String[] args) throws Exception {
        // 解析定义的 Interceptor类 和相关的注解
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class);
        // 加载字节码
        ClassNode classNode = AsmUtils.loadClass(UserService.class);
        // 对加载到的字节码做增强处理
        for (MethodNode methodNode : classNode.methods) {
            if (!methodNode.name.equals("findById")) continue;

            MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
            for (InterceptorProcessor interceptor : processors) {
                interceptor.process(methodProcessor);
            }
        }
        // 获取增强后的字节码
        byte[] bytes = AsmUtils.toBytes(classNode);

        // 查看反编译结果
        System.out.println(Decompiler.decompile(bytes));
        AgentUtils.reTransform(UserService.class, bytes);

        UserService sample = new UserService();
        sample.findById(1);


    }

对应的增强后的字节码和耗时增强逻辑结果如下:

为了更好的理解arthas的设计理念和逻辑,笔者也基于上述的demo示例进行设计和封装一个字节码增强工具类Enhancer。辅助我们更好的理解arhtas复杂的源码设计和实现,可以看到通过ByteKit本质上就是要求我们基于注解和方法给出需要声明的逻辑,然后AsmUtils会加载目标类的字节码修改目标方法的逻辑。

所以我们的Enhancer对于参数的封装要做到如下几点:

  1. 明确要求用户传入需要增强的类类型
  2. 通过抽象类做好注解声明并规范用户要实现的方法
  3. 明确给出set集合让用户指明需要增强的方法

有了上述的参数,我们的增强工具类就可以做到:

  1. 通过传入的累加器的类类型解析得出拦截处理器
  2. 加载目标字节码文件
  3. 通过set过滤出需要增强的目标方法
  4. 修改目标方法的字节码
  5. 返回字节码文件

所以基于这个思路我们封装出了拦截器的抽象类,告知用户可灵活继承实现的扩展点:

csharp 复制代码
/**
 * 规范性约束
 */
public abstract class AbstractInterceptor {
    /**
     * 函数入口增强点
     */
    static void atEnter() {
        //nothing to do
    }

    /**
     * 函数退出扩展点
     */
    static void atExit() {
        //nothing to do
    }
}

对应的我们的sample继承关系就变为这样:

csharp 复制代码
public class SampleInterceptor extends AbstractInterceptor {

    public static long start;

    @AtEnter(inline = true)
    public static void atEnter() {
        //记录开始时间
        start = System.currentTimeMillis();
    }

    @AtExit(inline = true)
    public static void atEit() {
        //输出打印被增强的方法总耗时
        System.out.println("cost:" + (System.currentTimeMillis() - start));
    }
}

在此基础上我们封装一个字节码增强的工具类,即:

  1. 通过interceptorClzz生成处理器processorList
  2. 通过AsmUtils加载目标类的字节码
  3. 通过matcherMethod匹配目标方法,并通过processorList修改字节码完成增强
scss 复制代码
/**
 * 字节码增强工具类
 */
public class Enhancer {
    public byte[] enhance(Class target,
                          Set<String> matcherMethod,
                          Class<? extends AbstractInterceptor> clzz) throws Exception {
        // 解析定义的 Interceptor类 和相关的注解
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processorList = interceptorClassParser.parse(clzz);
        // 加载需要增强的类的字节码
        ClassNode classNode = AsmUtils.loadClass(target);
        // 对加载到的字节码做增强处理
        for (MethodNode methodNode : classNode.methods) {
            //判断是否是匹配的方法,如果不是不做增强
            if (!matcherMethod.contains(methodNode.name)) {
                continue;
            }
            //基于解析后的processorList对目标进行增强
            MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
            for (InterceptorProcessor interceptor : processorList) {
                interceptor.process(methodProcessor);
            }
        }
        // 获取增强后的字节码
        byte[] bytes = AsmUtils.toBytes(classNode);

        return bytes;
    }

    
}

最后我们再给出使用示例:

arduino 复制代码
 public static void main(String[] args) throws Exception {
        Enhancer enhance = new Enhancer();
        byte[] bytes = enhance.enhance(UserService.class,
                CollUtil.newHashSet("findById"),
                SampleInterceptor.class);

        // 查看反编译结果
        System.out.println(Decompiler.decompile(bytes));
        AgentUtils.reTransform(UserService.class, bytes);
        //测试修改后的结果是否打印输出耗时
        UserService userService = new UserService();
        userService.findById(1);
    }

同时我们也给出对应的输出结果:

详解arthas工作全流程

arthas boot启动

宏观流程说明

通过上述的铺垫,我们已经对arthas运用的核心技术有了初步的了解,接下来笔者将从源码的江都深入分析这一点,首先我们需要从arthas-boot这个程序说起,本质上当我们执行as.sh后,执行脚本就是启动一个arthas-boot进程,对应的执行核心流程为:

  1. 解析用户参数
  2. 获取非arthas进程以外的程序,并生成列表在控制台输出
  3. 等待用户输入需要指定进程序号
  4. 将目标进程号、arhtas-corearthas-agent路径作为参数,启动arthas-core.jar让其完成后续工作

如下图,笔者传入--telnet-port 9999启动arthas-bootarhtas-boot启动时就会解析参数,然后通过执行jps指令获取非本进程的程序pid,然后生成列表,等待用户选择:

最后将pid号和arhtas-core和arthas一并作为参数启动arthas-core,就像下面这样:

bash 复制代码
java -jar /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -pid  61223  -telnet-port 9999, -core /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -agent  /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-agent.jar 

参数解析

对应的笔者也给出对应源码来印证这一点,上述的主流程都位于arthas-bootBootstrapmain方法,因为逻辑比较长所以笔者这里逐步拆解分析,首先是参数解析,例如笔者上述传入的telnet参数就会在这里完成解析并绑定到bootstrap

swift 复制代码
  CLI cli = CLIConfigurator.define(Bootstrap.class);
        //--telnet-port 9999
        CommandLine commandLine = cli.parse(Arrays.asList(args));

        try {
            //将参数绑定到bootstrap 此时bootstrap属性就会添加配置的参数值
            CLIConfigurator.inject(commandLine, bootstrap);
        } catch (Throwable e) {
            //......
        }

获取目标进程

然后就是查看用户启动参数中是否指明了pid,若没有则调用ProcessUtilsselect通过jps指令生成进程列表让用户选择:

scss 复制代码
  //查看参数是否指明了pid,若没有
        long pid = bootstrap.getPid();
        // 若没有指明pid,则调用select输出所有进程让用户选择
        if (pid < 0) {
            try {
              //调用jps生成进程列表等待用户选择
                pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid, bootstrap.getSelect());
            } catch (InputMismatchException e) {
               //......
            }
           //......
        }

对应的笔者也给出ProcessUtilsselect的具体实现展开说明:

  1. 通过jps获取非本进程以外的java进程
  2. 生成pid为key,进程pid+启动类的字符串value作为值如57844 org.jetbrains.jps.cmdline.Launcher的map集合
  3. 按顺序将其输出
  4. 利用Scanner工具类等待用户终端输入序号
  5. 返回对应pid,执行后续步骤:

对应源码如下,大体逻辑和上述说明一致即通过jps获取所有进程,并生成map映射按序输出,随后基于用户选择结果返回指定pid号:

arduino 复制代码
public static long select(boolean v, long telnetPortPid, String select) throws InputMismatchException {
        //执行jps指令生成pid为key 进程详情的value的map
        Map<Long, String> processMap = listProcessByJps(v); 
        //......
        // 将进程按序打印,从1开始,方便用户直观选择进程
        int count = 1;
        for (String process : processMap.values()) {
            if (count == 1) {
                System.out.println("* [" + count + "]: " + process);
            } else {
                System.out.println("  [" + count + "]: " + process);
            }
            count++;
        }

        // 等待用户选择
        String line = new Scanner(System.in).nextLine();
        if (line.trim().isEmpty()) {
            // get the first process id
            return processMap.keySet().iterator().next();
        }

        int choice = new Scanner(line).nextInt();

        //......
        //通过数字定位到具体pid号返回
        Iterator<Long> idIter = processMap.keySet().iterator();
        for (int i = 1; i <= choice; ++i) {
            if (i == choice) {
                return idIter.next();
            }
            idIter.next();
        }

        return -1;
    }

整合参数启动arthas-core

有了上述解析的参数和pid进程号,arthas-boot就会基于这些信息启动arthas-core,让其完成后续的核心工作(工作内容笔者后文会展开说明),这里我们先看看artahs-core的最重要的一步arthas-core启动的实现,从源码可以看到其内部会通过attachArgs顺序拼接各种参数,然后生成类似于如下的启动指令启动core:

bash 复制代码
java -jar /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -pid  61223  -telnet-port 9999, -core /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -agent  /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-agent.jar

对应逻辑比较简单,就是拼接和解析参数生成启动指令启动jar的逻辑,这里笔者就不多做赘述了:

erlang 复制代码
if (telnetPortPid > 0 && pid == telnetPortPid) {
            AnsiLog.info("The target process already listen port {}, skip attach.", bootstrap.getTelnetPortOrDefault());
        } else {
            //double check telnet port and pid before attach
            telnetPortPid = findProcessByTelnetClient(arthasHomeDir.getAbsolutePath(), bootstrap.getTelnetPortOrDefault());
            checkTelnetPortPid(bootstrap, telnetPortPid, pid);

            // start arthas-core.jar
            //拼接jar和arhtas-boot路径
            List<String> attachArgs = new ArrayList<String>();
            attachArgs.add("-jar");
            attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
            attachArgs.add("-pid");
            attachArgs.add("" + pid);
            //......
            //拼接我们添加的telnet参数
            if (bootstrap.getTelnetPort() != null) {
                attachArgs.add("-telnet-port");
                attachArgs.add("" + bootstrap.getTelnetPort());
            }

            if (bootstrap.getHttpPort() != null) {
                attachArgs.add("-http-port");
                attachArgs.add("" + bootstrap.getHttpPort());
            }
            //拼接core和agent的路径绝对路径地址
            attachArgs.add("-core");
            attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
            attachArgs.add("-agent");
            attachArgs.add(new File(arthasHomeDir, "arthas-agent.jar").getAbsolutePath());
            //......

            AnsiLog.info("Try to attach process " + pid);
            AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);
            //基于上述attachArgs生成启动命令启动arthas-core
            ProcessUtils.startArthasCore(pid, attachArgs); //java -jar /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -pid  61223  -telnet-port 9999, -core /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-core.jar  -agent  /Users/sharkchili/.arthas/lib/4.1.3/arthas/arthas-agent.jar

            AnsiLog.info("Attach process {} success.", pid);
        }

自此我们来小结一下,arthas-boot启动步骤:

  1. 解析启动参数
  2. 获取所有java进程生成列表等待用户选择
  3. 基于选择的pid号和agent和core两个核心模块的绝对路径生成arthas-core指令启动arhtas-core

arthas-core 启动

执行流程和调试环境说明

我们重点还是关注一下arthas-core的逻辑,它是动态增强并实现动态插桩增强的关键,其内部本质工作本质就是将arthas-agent attach到上一步选定的进程进行动态增强。

为了能够调测这段源代码,笔者针对这段调测过程进行一个简要的说明,本质上通过上一步的arthas-boot会启动artahs-core让其将arthas-agent能够attach到运行时的目标虚拟机上,所以为了能够调测的attach的逻辑,arthas的作者也在启动脚本上做了一些手脚。

首先我们需要通过as脚本指定--debug-attacharthas-boot启动,然后我们按照说明选择目标进程,以笔者为例就是一个demo进程,此时远程的arhtas-core就会开启调试模式监听8888端口等待idea远程attach,对应我们idea通过远程调试模式设置8888端口点击执行,此时我们idea就可以开始调测了解arhtas-core如何attach代理工具arthas-agent的逻辑了:

了解整体思路之后,我们就开始搭建调测环境,首先我们编写一个demo程序,并将其通过javac编译:

csharp 复制代码
public class Demo {
    static class Counter {
        private static AtomicInteger count = new AtomicInteger(0);
        public static void increment() {
            count.incrementAndGet();
        }
        public static int value() {
            return count.get();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            Counter.increment();
            System.out.println("counter: " + Counter.value());
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

然后通过jdk8的java指令启动:

bash 复制代码
/Users/sharkchili/dev-tool/jdk8/Contents/Home/bin/java  Demo

随后,我们再打开arthas源码包bin在终端执行如下指令,并选择demo进程的序号连接:

css 复制代码
./as.sh --use-version 3.6.0 --debug-attach

最后idea配置远程模式attach8888端口:

由此我们就可以愉快的调试arthas-core了:

对应调试步骤笔者也是参考这个issue,感兴趣的读者可以看看:github.com/alibaba/art...

工作流程介绍

arthas-core(以下简称为core)的逻辑比较简单,通过上文介绍我们知道arthas-boot在启动core的时候给定了一些参数,定位目标虚拟机pid号,通过agent参数指明的arthas-agent.jar attach到该虚拟机进程下。

对应源代码入口位于arhtas-core包的Arthas的main方法,其内部就是创建一个arthas对象,本质就是执行:

  1. parse解析获取所有启动core的参数
  2. 调用attach将参数给定路径的agent attach到目标虚拟机进程(也就是我们的demo)
typescript 复制代码
  public static void main(String[] args) {
        try {
            new Arthas(args);
        } catch (Throwable t) {
          //......
        }
    }

private Arthas(String[] args) throws Exception {
        attachAgent(parse(args));
    }

我们直接查看attachAgent可以看到笔者说明的3块核心逻辑:

  1. 通过VirtualMachine.list()定位所有java进程,遍历匹配到目标进程的虚拟机描述符
  2. 通过attach连接到目标虚拟机进程
  3. 基于配置定位arhtas-agent本地路径,通过loadAgentarthas-agent加载到目标虚拟机上

逻辑比较简单,读者可以结合注释理解笔者上面所说的步骤,本质就是基于参数将agent attach到目标虚拟机进程,开始后续最最核心的字节码增强实现各种监控诊断指令:

ini 复制代码
private void attachAgent(Configure configure) throws Exception {
        VirtualMachineDescriptor virtualMachineDescriptor = null;
        //定位到被代理的虚拟机进程号
        for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
            String pid = descriptor.id();
            if (pid.equals(Long.toString(configure.getJavaPid()))) {
                virtualMachineDescriptor = descriptor;
                break;
            }
        }
        //将
        VirtualMachine virtualMachine = null;
        try {
            if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
                virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
            } else {
                virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
            }

            //......
     //定位agent绝对路径
            String arthasAgentPath = configure.getArthasAgent();
            //convert jar path to unicode string
            configure.setArthasAgent(encodeArg(arthasAgentPath));
            configure.setArthasCore(encodeArg(configure.getArthasCore()));
            try {//加载arthas-agent.jar,对当前进程进行增强操作
                virtualMachine.loadAgent(arthasAgentPath,
                        configure.getArthasCore() + ";" + configure.toString());
            } catch (IOException e) {
                //......
            }
        } finally {
            //......
        }
    }

arthas agent 增强(重点)

宏观流程介绍

自此我们就可以正式的开始分析arthas最核心的一个部分,即动态增强了,通过上述的步骤我们将artahs-agent attach到指定pid的虚拟机中,接下来我们就要通过源码的分析的方式了解,attach后的agent做了那些事情。

当arthas-agent attach目标jvm之后,就需要做到拦截所有类确保在必要时刻能够做到运行时动态增强,这时就需要用到agentmain方法,即通过配置中指定Agent-Class,确保加载到目标虚拟机的agent可以执行对应agentmain方法,执行必要的初始化流程(后文会展开详解),生成一个arthas服务端监听3658端口,等待客户端连接:

调试环境搭建

为了更好的了解这个流程,笔者也介绍一下关于arthas-agent attach的详细流程,首先我们需要启动上述的demo并以监听模式运行,因为笔者环境默认jdk为jdk11,所以这里手动的指定了一下jdk版本:

bash 复制代码
 /Users/sharkchili/dev-tool/jdk8/Contents/Home/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000 Demo

然后arhtas源码通过远程连接的方式连接8000端口:

最后我们启动as脚本,并选择demo进程,执行到arhtas attach环节时,代码就会来到arhtas agent 包下AgentBootstrap的agentMain方法:

attach流程说明

从上文的调试截图可以看到,其内部本质就是拿着Instrumentation引入动态增强的逻辑,我们步入main方法可以看到,其内部核心逻辑为:

  1. 拉取core路径获取对应的类加载器
  2. 调用core包的类加载ArthasBootstrap.class
  3. 基于ArthasBootstrap.class初始化启动arhtas服务端监听3658端口(默认情况下)

对应main的源码如下,他会从args中获取agent和core的路径然后拉取加载器启动一个bindingThread启动shellServer也就是arthas服务端:

ini 复制代码
private static synchronized void main(String args, final Instrumentation inst) {
        // 尝试判断arthas是否已在运行,如果是的话,直接就退出
       
        try {
            ps.println("Arthas server agent start...");
            // 传递的args参数分两个部分:arthasCoreJar路径和agentArgs, 分别是Agent的JAR包路径和期望传递到服务端的参数
            if (args == null) {
                args = "";
            }
            args = decodeArg(args);
     //获取core包合agent包路径
            String arthasCoreJar;
            final String agentArgs;
            int index = args.indexOf(';');
            if (index != -1) {
              //从参数中获取arhtas core jar 路径
                arthasCoreJar = args.substring(0, index); 
                //从参数中获取arthas agent jar路径
                agentArgs = args.substring(index);
            } else {
                arthasCoreJar = "";
                agentArgs = args;
            }


             */
             //获取core jar的类加载器
            final ClassLoader agentLoader = getClassLoader(inst, arthasCoreJarFile);

            Thread bindingThread = new Thread() {
                @Override
                public void run() {
                    try {
                        bind(inst, agentLoader, agentArgs);//启动arthas服务端
                    } catch (Throwable throwable) {
                        throwable.printStackTrace(ps);
                    }
                }
            };

            bindingThread.setName("arthas-binding-thread");
            bindingThread.start();
            bindingThread.join();
        } catch (Throwable t) {
          //......
        }
    }

此时逻辑就到了getInstancegetInstance单例实现,其内部会解析参数完成完成如下工作:

  1. initFastjson:初始化json工具规则配置
  2. initSpy(重点):将SpyAPI添加到当前bootstrap类加载器中
  3. initArthasEnvironment:初始化arthas环境变量
  4. 创建arthas日志输出文件夹arthas-output
  5. LogUtil初始化日志工具
  6. 增强ClassLoader:解决解决一些 ClassLoader 加载不到 SpyAPI的问题所以才要增强ClassLoader
  7. init beans:初始化一些视图窗口消息输入输出解析器和history指令管理器
  8. bind:重点启动arthas server
  9. 初始化executorService线程池执行后监听并处理后续用户的监控诊断指令的异步任务
  10. 创建一个shutdown线程注册虚拟机钩子监听关闭事件以便及时关闭ArthasBootstrap
scss 复制代码
private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
        this.instrumentation = instrumentation;
    //初始化json工具规则配置
        initFastjson();

        // 2. 将SpyAPI添加到当前bootstrap类加载器中
        initSpy();
        // 3. 初始化arthas环境变量
        initArthasEnvironment(args);
   //4. 创建arthas日志输出目录
        String outputPathStr = configure.getOutputPath();
        if (outputPathStr == null) {
            outputPathStr = ArthasConstants.ARTHAS_OUTPUT;
        }
        outputPath = new File(outputPathStr);
        outputPath.mkdirs();

        // 5. 初始化日志工具
        loggerContext = LogUtil.initLogger(arthasEnvironment);

        // 4. 增强ClassLoader
        enhanceClassLoader();
        // 5. 初始化一些视图解析器和history管理bean
        initBeans();

        // 6. 启动agent server
        bind(configure);
    //初始化命令解析的线程池
        executorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                final Thread t = new Thread(r, "arthas-command-execute");
                t.setDaemon(true);
                return t;
            }
        });
   //注册虚拟机钩子
        shutdown = new Thread("as-shutdown-hooker") {

            @Override
            public void run() {
                ArthasBootstrap.this.destroy();
            }
        };

        transformerManager = new TransformerManager(instrumentation);
        Runtime.getRuntime().addShutdownHook(shutdown);
    }

详解ArthasBootstrap启动核心流程

详解spy初始化(重点)

上文宏观说明了ArthasBootstrap整体流程,因为整体流程实现细节较多,所以笔者打算抽取一个篇幅逐步拆解这块流程,先来说说initSpy,该流程本质就是通过根加载器将搜寻SpyAPI这个class是否存在,若不存在则定位到对应的jar包物理地址将其添加到当前虚拟机(例如我们需要增强的Demo)程序:

这里我们需要了解两个问题:

  1. 为什么需要加载SpyAPI
  2. 为什么需要通过根加载器进行加载

为什么需要加载SpyAPI

我们查看SpyAPI是arthas增强的核心抽象类,它是增强操作逻辑的具体实现者,arhtas在进行增强时本质就是通过SpyAPI的调用实现的,SpyAPI内置了一个spyInstance,当我们针对特定实现类通过stack或者trace指令针对类进行动态增强观察其堆栈或者耗时时,本质就是通过SpyAPI对应atEnter的方法定位到每个指令的监听器执行函数增强。

例如我们针对Demo类increment执行watch指令,arhtas的增强操作就是通过增强器在方法前后插入SpyAPI的调用,每当程序执行increment操作时就会调用SpyAPI的atEnter、atExit等方法,而这些方法就会搜寻到watch指令的监听器并执行对应逻辑:

对应我们这里也先给出SpyAPI的源代码,可以看到其内部暴露了setSpy及其atEnter等增强函数,其内部本质都是调用spyInstance通过atEnter获取对应指令的监听器执行前置或者后置的增强逻辑:

java 复制代码
public class SpyAPI {
    public static final AbstractSpy NOPSPY = new NopSpy();
    private static volatile AbstractSpy spyInstance = NOPSPY;

    public static volatile boolean INITED;

    //......
  public static void setSpy(AbstractSpy spy) {
        spyInstance = spy;
    }

    public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        spyInstance.atEnter(clazz, methodInfo, target, args);
    }
}

为什么需要通过根加载器进行加载

通常情况下,java类加载器分为如下几种:

  1. bootstrap类加载器:由c++实现,是虚拟机自身的一部分,主要负责加载lib目录下存放的类
  2. extension类加载器: 主要加载lib目录下ext目录中的类
  3. application 应用加载器,主要加载用户类路径下所有的类库

按照JVM双亲委派模型的说法,当进行类加载时,所有类都会优先将请求委托给父类,明确父类搜寻范围找不到对应类而无法完成加载后,才会自己尝试加载,由此保证rt.jar等基础核心jar包类加载安全,同理,为了避免加载spy这个核心增强的接口被破坏,arthas就会强制指明用根加载器完成加载,确保所有类都能够准确的加载到这个类:

ini 复制代码
 // 将Spy添加到BootstrapClassLoader
        ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
        Class<?> spyClass = null;
        if (parent != null) {
            try {
                spyClass =parent.loadClass("java.arthas.SpyAPI");
            } catch (Throwable e) {
                // ignore
            }
        }

启动arthas server

我们重点还是关注一下启动arthas server这一步,该步骤由bind方法负责,工作线程在cas成功后上锁后,顺序执行如下步骤:

  1. 会通过读取启动参数确认启动方式(默认是telnet server对应3658端口)的配置创建shellServer
  2. 初始化创建命令集合对象BuiltinCommandPack,其内部包含例如jad即JadCommand、stack即StackCommand这些命令的抽象实现类
  3. 基于netty创建workerGroup作为服务端的连接处理工作线程组
  4. 初始化server对象并添加到termServers容器中
  5. 变量termServers调用listen开启服务端监听

对应完成后的服务端架构图如下所示,可以看到shellServer用termServers维护不同的网络服务端,同时用sessions维护用户会话,而常见的命令抽象也都以类似于BuiltinCommandPack这样的封装集合容器维护到resolvers容器中:

对应我们也给出bind对应实现,正如笔者所说在cas修改状态保证线程互斥后,初始化shellServer完成:

  1. 服务端初始化
  2. 命令初始化
  3. 启动服务端监听
scss 复制代码
private void bind(Configure configure) throws Throwable {

        long start = System.currentTimeMillis();

        if (!isBindRef.compareAndSet(false, true)) {//cas 改变状态
            throw new IllegalStateException("already bind");
        }

        //......

        try { //初始化shell server 欢迎输出等信息
            ShellServerOptions options = new ShellServerOptions()
                            .setInstrumentation(instrumentation)
                            .setPid(PidUtils.currentLongPid()) //atacch 的pid
                            .setWelcomeMessage(ArthasBanner.welcome());
            if (configure.getSessionTimeout() != null) {
                options.setSessionTimeout(configure.getSessionTimeout() * 1000);
            }

      //......
            shellServer = new ShellServerImpl(options);

          //......
          //初始化BuiltinCommandPack内部包含jad、monitor等执行的抽象
            BuiltinCommandPack builtinCommands = new BuiltinCommandPack(disabledCommands);//绑定命令
            List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
            resolvers.add(builtinCommands);

            //worker group
            workerGroup = new NioEventLoopGroup(new DefaultThreadFactory("arthas-TermServer", true));

            // 基于配置创建并注册HttpTelnetTermServer、HttpTermServer等服务端
            if (configure.getTelnetPort() != null && configure.getTelnetPort() > 0) {
                logger().info("try to bind telnet server, host: {}, port: {}.", configure.getIp(), configure.getTelnetPort());
                shellServer.registerTermServer(new HttpTelnetTermServer(configure.getIp(), configure.getTelnetPort(),
                        options.getConnectionTimeout(), workerGroup, httpSessionManager));
            } else {
                logger().info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
            }
            if (configure.getHttpPort() != null && configure.getHttpPort() > 0) {
                logger().info("try to bind http server, host: {}, port: {}.", configure.getIp(), configure.getHttpPort());
                shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
                        options.getConnectionTimeout(), workerGroup, httpSessionManager));
            } else {
                // listen local address in VM communication
                if (configure.getTunnelServer() != null) {
                    shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
                            options.getConnectionTimeout(), workerGroup, httpSessionManager));
                }
                logger().info("http port is {}, skip bind http server.", configure.getHttpPort());
            }
     //遍历resolvers集合中的命令包将其添加到shellServer的命令管理容器中
            for (CommandResolver resolver : resolvers) {
                shellServer.registerCommandResolver(resolver);
            }
     //内部遍历刚刚注册的服务端并启动监听
            shellServer.listen(new BindHandler(isBindRef));//监听
            //......
        } catch (Throwable e) {
         //......

        }
    }

笔者这里也给出对应监听的实现,本质上就是遍历各个server并配置消息处理器TermServerTermHandler执行listen监听并通过TermServerTermHandler处理请求:

kotlin 复制代码
@Override
    public ShellServer listen(final Handler<Future<Void>> listenHandler) {
        final List<TermServer> toStart;
        synchronized (this) {
           //......
            toStart = termServers;
        }
        
       //......
        for (TermServer termServer : toStart) {//遍历启动创建的对应网络通信方式的server
            termServer.termHandler(new TermServerTermHandler(this));//服务器server绑定 terminal handler
            termServer.listen(handler); //监听网络请求,收到的请求就会交由TermServerTermHandler也就是上一步初始化的处理器处理请求
        }
        return this;
    }

客户端建立连接

明确上述服务端建立之后,arthas会为我们分配一个客户端与其建立连接,分别执行:

  1. 创建会话session
  2. 终端输出欢迎信息
  3. 将会话保存到服务端缓存sessions
  4. 监听客户端输入,并通过ShellLineHandler处理请求

对应笔者给出HttpTelnetTermServer处理客户端连接处理的入口:

typescript 复制代码
@Override
    public TermServer listen(Handler<Future<TermServer>> listenHandler) {
       //初始化bootstrap
        bootstrap = new NettyHttpTelnetTtyBootstrap(workerGroup, httpSessionManager).setHost(hostIp).setPort(port);
        try {
            bootstrap.start(new Consumer<TtyConnection>() {
                @Override
                public void accept(final TtyConnection conn) {
                  //监听TermServerTermHandler收到的请求并调用handle方法处理
                    termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
                }
            }).get(connectionTimeout, TimeUnit.MILLISECONDS);
             //......
        } catch (Throwable t) {
           //......
        }
        return this;
    }

TermServerTermHandler最终会执行到ShellServerImplhandleTerm完成上文所说的session初始化和readline监听命令请求的核心逻辑,逻辑比较直观,读者可以结合注释了解:

scss 复制代码
public void handleTerm(Term term) {
        synchronized (this) {//保证停止后可以及时停止连接
            // That might happen with multiple ser
            if (closed) {
                term.close();
                return;
            }
        }
   // 创建本地会话session,为其分配命令列表、instrumentation(引入spy的代码插桩实例)
        ShellImpl session = createShell(term);
       //......
        //终端输出欢迎信息
        session.init();
        //缓存session信息
        sessions.put(session.id, session); 
        //监听用户的输入命令
        session.readline(); 
    }

基于stack了解arthas设计理念与工作机制

在建立服务端之后,对应会话session.readline();监听客户端请求,例如笔者希望通过stack指令了解increment方法的执行堆栈信息:

arduino 复制代码
stack Demo$Counter increment  -n 5 

其实session收到请求后会通过ShellLineHandler处理该请求,对应ShellLineHandler内部执行核心逻辑为:

  1. 解析指令token获取指令字符串,对应本次示例就是stack字符串
  2. 基于指令字符串stack获取对应command对应本例就是StackCommand并封装成ProcessImpl
  3. 基于上述ProcessImpl处理器、session会话等信息构成JobImpl
  4. JobImpl执行run方法将上文的ProcessImpl(包含命令以及命令上下文信息)封装成CommandProcessTask提交到上文初始化好的线程池executorService
  5. 重点来了,异步任务运行后StackCommand对应抽象父类EnhancerCommand对应enhance逻辑会针对命令对应的类执行插桩操作,即通过上文说明的ByteKit这个工具封装的而成的InterceptorProcessor针对目标类匹配方法进行字节码增强,本质上就是结合当前指令(例如本例的stack)获取对应监听器,以当前类作为key,监听器列表作为value缓存到全局map中,后续完成插桩后的类执行atEnter调用时就会通过全局监听器定位到当前类的监听器完成执行增强逻辑:

了解宏观流程后,我们给出ShellLineHandlerhandle方法,本质就是解析传入的命令line,然后按照上面说的解析命令等信息封装成process(包含命令抽象和session)从而构建出job,然后调用run方法:

scss 复制代码
@Override
    public void handle(String line) {//stack Demo$Counter increment
        if (line == null) {
            // EOF
            handleExit();
            return;
        }

        List<CliToken> tokens = CliTokens.tokenize(line);
        CliToken first = TokenUtils.findFirstTextToken(tokens);//拿到一个 token得到对应的指令
       //......

        Job job = createJob(tokens);
        if (job != null) {
            job.run();//提交到线程池
        }
    }

由此来到JobImpl的run方法,由此看到最核心的逻辑,即将process处理器封装到CommandProcessTask提交到线程池中运行:

arduino 复制代码
@Override
    public Job run(boolean foreground) {
   //......
        process.setSession(this.session);
        process.run(foreground);

    //......
        
        return this;
    }


@Override
    public synchronized void run(boolean fg) {
        //......
        Runnable task = new CommandProcessTask(process);//封装成任务
        ArthasBootstrap.getInstance().execute(task); //丢到无界线程池中
    }

经过层层抽象调用来到执行stackCommandprocess,即执行抽象父类的通用增强逻辑:

  1. 匹配定位要拦截的方法,插入SpyAPIatEnter等方法完成字节码增强
  2. 定位子类即stackCommand的监听器以当前目标类为key,监听器为value缓存到全局监听器
  3. 返回增强后的字节码
scss 复制代码
 @Override
    public byte[] transform(final ClassLoader inClassLoader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
           //......

          

           //拿到原有的代码的字节码
            ClassNode classNode = new ClassNode(Opcodes.ASM9);
            ClassReader classReader = AsmUtils.toClassNode(classfileBuffer, classNode);
           
            // 生成增强字节码
            DefaultInterceptorClassParser defaultInterceptorClassParser = new DefaultInterceptorClassParser();

            //......

     //过滤匹配需要增强的目标方法
            List<MethodNode> matchedMethods = new ArrayList<MethodNode>();
            for (MethodNode methodNode : classNode.methods) {
                if (!isIgnore(methodNode, methodNameMatcher)) {
                    matchedMethods.add(methodNode);
                }
            }

              //......

           

            for (MethodNode methodNode : matchedMethods) {
                
                // 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener
                if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) {
                     //......
                }else {
                    MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter);
                    //遍历所有interceptor针对匹配的目标方法进行增强操作
                    for (InterceptorProcessor interceptor : interceptorProcessors) {
                        try {
                            List<Location> locations = interceptor.process(methodProcessor);
                            for (Location location : locations) {
                                if (location instanceof MethodInsnNodeWare) {
                                    MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location;
                                    MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode();
//针对当前类注册监听器,对应该方法底层就会以当前类作为key,添加一个相关指令的监听器(例如stack就是StackAdviceListener)等待执行增强逻辑执行时执行栈帧调用追踪
                                     AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className,
                                            methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener);
                                }
                            }

                        } catch (Throwable e) {
                           //......
                        }
                    }
                }

 //针对当前类注册监听器,例如本次用stack指令对Demo类的Counter内部类的increment做调用追踪,对应该方法底层就会以当前类作为key,添加一个StackAdviceListener等待执行增强逻辑执行时执行栈帧调用追踪
                AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc,
                        listener);
                //......
            byte[] enhanceClassByteArray = AsmUtils.toBytes(classNode, inClassLoader, classReader);

          //......
    //返回增强后的字节码

            return enhanceClassByteArray;
        } catch (Throwable t) {
            logger.warn("transform loader[{}]:class[{}] failed.", inClassLoader, className, t);
            affect.setThrowable(t);
        }

        return null;
    }

解耦化拦截类方法执行增强逻辑

后续对于该类即Demo.Counter的调用,就会执行到SpyAPIatEnter方法,本质上就是对应spyInstance(也就是SpyImpl)的调用:

typescript 复制代码
public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        spyInstance.atEnter(clazz, methodInfo, target, args);
    }

步入SpyImpl源代码可以看到,其底层就会通过全局监听器定位到这个类的监听器完成具体的增强逻辑,显见arthas作者对于解耦思想的见解:

ini 复制代码
@Override
    public void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
        ClassLoader classLoader = clazz.getClassLoader();

        String[] info = StringUtils.splitMethodInfo(methodInfo);
        String methodName = info[0];
        String methodDesc = info[1];
       //基于当前类到全局缓存获取所有监听器
        List<AdviceListener> listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(),
                methodName, methodDesc);
        if (listeners != null) {
            for (AdviceListener adviceListener : listeners) {
                try {
                    if (skipAdviceListener(adviceListener)) {
                        continue;
                    }
                    //执行匹配的监听器逻辑
                    adviceListener.before(clazz, methodName, methodDesc, target, args);
                } catch (Throwable e) {
                    logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e);
                }
            }
        }

    }

以本次的stack为例也就是在调用前在当前线程内部维护一个开始时间,这里笔者考虑到stack指令介绍的完整性也将atExit注解对应执行的afterReturning方法,可以看到stack本质要做的就是:

  1. before记录其实时间
  2. afterReturning通过finishing提取线程中的调用堆栈信息和线程信息封装成StackModel输出
java 复制代码
 @Override
    public void before(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args)
            throws Throwable {
        // 开始计算本次方法调用耗时
        threadLocalWatch.start();
    }

@Override
    public void afterReturning(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args,
                               Object returnObject) throws Throwable {
        Advice advice = Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject);//获取类信息 入参 出参信息
        finishing(advice);
    }

    private void finishing(Advice advice) {
        // 本次调用的耗时
        try {
            double cost = threadLocalWatch.costInMillis();
            //判断条件是否满足要输出的结果
            boolean conditionResult = isConditionMet(command.getConditionExpress(), advice, cost);
         //......
            if (conditionResult) {
                //提取当前线程内部维护的stacktrace构成调用链以及各种线程信息构成StackModel返回
                StackModel stackModel = ThreadUtil.getThreadStackModel(advice.getLoader(), Thread.currentThread());
                stackModel.setTs(new Date());
                process.appendResult(stackModel);
              //......
            }
        } catch (Throwable e) {
           //......
        }
    }

最终我们就可以看到类似下面这样的输出结果:

关于arthas性能问题的探讨

在笔者了解arthas这本技术的时候,看到github有这样一个话题:

复制代码
arthas进行增强时,对于程序执行性能是否有影响

参考大部分答案结合笔者的经验,针对那些被JIT优化且生成机器码的字节码进行增强时,这就会使得原有的缓存失效,使得被增强的类经历:

  1. 字节码修改
  2. 原有JIT优化后的代码失效
  3. 高并发的函数调用会调起抖动
  4. 再次经历解释执行、JIT编译再优化为编译版本的机器码执行

从使用的表现来看,attach因为增强所有的classloader的那一瞬间CPU使用率确实会短暂飙升,出现服务抖动,所以在高并发的生产环境还是慎用。

感兴趣的读者可以移步了解该问题:Arthas attach 之后对原进程性能有多大的影响:github.com/alibaba/art...

小结

本文基于arthas 3.6.0源码针对arthas运行时增强技术进行深入分析,可以看到这个监控诊断工具利用了agent的attach进行运行时通过字节码进行通用的字节码修改增强,然后根据用户键入的指令获取对应监听器缓存到全局,在执行通用字节码即SpyAPI调用时就会定位到指定监听器完成响应的逻辑增强。

你好,我是 SharkChili ,禅与计算机程序设计艺术布道者,希望我的理念对您有所启发。

📝 我的公众号:写代码的SharkChili

在这里,我会分享技术干货、编程思考与开源项目实践。

🚀 我的开源项目:mini-redis

一个用于教学理解的 Redis 精简实现,欢迎 Star & Contribute:
github.com/shark-ctrl/...

👥 欢迎加入读者群

关注公众号,回复 【加群】 即可获取联系方式,期待与你交流技术、共同成长!

参考

Arthas技术原理-源码调试环境搭建:wanglikang.github.io/2024/08/03/...

深入剖析Arthas源码:www.cnblogs.com/bigcoder84/...

Debug Arthas In IDEA :github.com/alibaba/art...

指定版本启动arthas:github.com/alibaba/art...

开源诊断利器Arthas ByteKit 深度解读(1):基本原理介绍:blog.csdn.net/hengyunabc/...

Java agent-05-Bytecode Kit-00-bytekit 入门介绍:zhuanlan.zhihu.com/p/129797960...

Java 程序的运行原理:www.cnblogs.com/luojw/p/181...

Class VirtualMachine API官方文档:docs.oracle.com/javase/8/do...

本文使用 markdown.com.cn 排版

相关推荐
2501_921649492 小时前
股票 API 对接, 接入德国法兰克福交易所(FWB/Xetra)实现量化分析
后端·python·websocket·金融·区块链
小兔崽子去哪了2 小时前
Java 登录专题
java·spring boot·后端
程序员小假3 小时前
学院本大二混子终于找到实习了...
java·后端
武子康3 小时前
大数据-194 数据挖掘 从红酒分类到机器学习全景:监督/无监督/强化学习、特征空间与过拟合一次讲透
大数据·后端·机器学习
大学生资源网3 小时前
基于springboot的智能家居系统的设计与实现(源码+文档)
java·spring boot·后端·毕业设计·源码
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue校园招聘系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
自由生长20243 小时前
windows上写C++的编译器选择和环境
后端
华仔啊3 小时前
都在用 Java8 或 Java17,那 Java9 到 16 呢?他们真的没用吗?
java·后端
WizLC3 小时前
【后端】面向对象编程是什么(附加几个通用小实例项目)
java·服务器·后端·python·设计语言