arthas底层实现原理剖析

Posted 黄小斜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了arthas底层实现原理剖析相关的知识,希望对你有一定的参考价值。

前言
经常在应用的启动或者运行过程中需要动态的查看数据,或者实时的验证我们写的代码的结构与执行过程,此时需要一种工具能够动态的检测程序运行的状态,内存数据,线程情况,最好能够动态的替换代码实时生效,方便我们从日志或者其他埋点断言我们的猜测。

1. arthas 阿尔萨斯的工程结构
其实有很多工具可以达到这种效果,arthas就是其中一种。

从工程结构,其实arthas的核心功能是core,里面有arthas的attach与诊断指令的代码。 通过实际启动分析进一步看原理。

2. arthas 启动
2.1 打包
对源码去除

git-commit-id-plugin
插件,毕竟现在github已经很难连接了

执行mvn clean package,在packing module下

src下面其实有

assembly.xml
文件定义了打包的详情,每个module定义了打包的插件,毕竟诊断工具需要把所有第三方的jar class字节码打进jar,即fatjar,所以对依赖需要尽量少,观源码arthas重度依赖Telnet netty,感觉依赖有点重。

2.2 执行boot启动
boot的启动是执行java -jar,其实就是一个普通的jar应用

2.2.1  选择进程pid
pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid, bootstrap.getSelect());
 其实很简单,就是去找java home,找到jps命令,然后jps -l

可以看到findJps

查找本机jvm进程 

 2.2.2 启动进程attach pid
ProcessUtils.startArthasCore(pid, attachArgs);
 不明白为啥要独立启动一个进程去attach,这个进程在attach完成后自动运行结束。参数就是前面的pid core agent等,其实核心是pid agent jar,其他都是额外功能的。

    public static void startArthasCore(long targetPid, List<String> attachArgs)
        // find java/java.exe, then try to find tools.jar
        String javaHome = findJavaHome();
 
        // find java/java.exe
        File javaPath = findJava(javaHome);
        if (javaPath == null)
            throw new IllegalArgumentException(
                            "Can not find java/java.exe executable file under java home: " + javaHome);
       
 
        File toolsJar = findToolsJar(javaHome);
 
        if (JavaVersionUtils.isLessThanJava9())
            if (toolsJar == null || !toolsJar.exists())
                throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome);
           
       
 
        List<String> command = new ArrayList<String>();
        command.add(javaPath.getAbsolutePath());
 
        if (toolsJar != null && toolsJar.exists())
            command.add("-Xbootclasspath/a:" + toolsJar.getAbsolutePath());
       
 
        command.addAll(attachArgs);
        // "$JAVA_HOME"/bin/java \\
        // $opts \\
        // -jar "$arthas_lib_dir/arthas-core.jar" \\
        // -pid $TARGET_PID \\
        // -target-ip $TARGET_IP \\
        // -telnet-port $TELNET_PORT \\
        // -http-port $HTTP_PORT \\
        // -core "$arthas_lib_dir/arthas-core.jar" \\
        // -agent "$arthas_lib_dir/arthas-agent.jar"
 
        ProcessBuilder pb = new ProcessBuilder(command);
        try
            final Process proc = pb.start();
这里严重依赖tools.jar,因为使用了里面虚拟机的attach方法

 启动里面的arthas-core.jar

那么执行com.taobao.arthas.core.Arthas

源码分析,VirtualMachine即tools的能力,所以前面需要查找tools.jar

    private void attachAgent(Configure configure) throws Exception
        VirtualMachineDescriptor virtualMachineDescriptor = null;
        //VirtualMachine.list() 相当于jps -lv的能力
        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);
           
 
            Properties targetSystemProperties = virtualMachine.getSystemProperties();
            String targetJavaVersion = JavaVersionUtils.javaVersionStr(targetSystemProperties);
            String currentJavaVersion = JavaVersionUtils.javaVersionStr();
            if (targetJavaVersion != null && currentJavaVersion != null)
                if (!targetJavaVersion.equals(currentJavaVersion))
                    AnsiLog.warn("Current VM java version: do not match target VM java version: , attach may fail.",
                                    currentJavaVersion, targetJavaVersion);
                    AnsiLog.warn("Target VM JAVA_HOME is , arthas-boot JAVA_HOME is , try to set the same JAVA_HOME.",
                                    targetSystemProperties.getProperty("java.home"), System.getProperty("java.home"));
               
           
 
            String arthasAgentPath = configure.getArthasAgent();
            //convert jar path to unicode string
            configure.setArthasAgent(encodeArg(arthasAgentPath));
            configure.setArthasCore(encodeArg(configure.getArthasCore()));
            //载入jar
            virtualMachine.loadAgent(arthasAgentPath,
                    configure.getArthasCore() + ";" + configure.toString());
        finally
            if (null != virtualMachine)
                //attach 完成后需要通知结束
                virtualMachine.detach();
           
       
   
2.2.3 attach后处理
attach pid后,会loadAgent,加载agent的jar

定义了

Premain-Class、Agent-Class、Can-Redefine-Classes、Can-Retransform-Classes
 Premain-Class、Agent-Class定义执行的main方法: Agent-Class是attach的方式;Premain-Class是agent随启动的执行方式

Can-Redefine-Classes、Can-Retransform-Classes 定义字节码增强的开关

获取classloader,然后bind,先看classloader

    private static ClassLoader getClassLoader(Instrumentation inst, File arthasCoreJarFile) throws Throwable
        // 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀
        return loadOrDefineClassLoader(arthasCoreJarFile);
   
 
    private static ClassLoader loadOrDefineClassLoader(File arthasCoreJarFile) throws Throwable
        if (arthasClassLoader == null)
            arthasClassLoader = new ArthasClassloader(new URL[]arthasCoreJarFile.toURI().toURL());
       
        return arthasClassLoader;
   
其实就是自定义classloader,载入jar包 

反射创建 

ArthasBootstrap实例
    private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable
        /**
         * <pre>
         * ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(inst);
         * </pre>
         */
        Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
        Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
        boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
        if (!isBind)
            String errorMsg = "Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.";
            ps.println(errorMsg);
            throw new RuntimeException(errorMsg);
       
        ps.println("Arthas server already bind.");
   
其实就是new 对象的时候,干些初始化的事情

    /**
     * 单例
     *
     * @param instrumentation JVM增强
     * @return ArthasServer单例
     * @throws Throwable
     */
    public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, Map<String, String> args) throws Throwable
        if (arthasBootstrap == null)
            arthasBootstrap = new ArthasBootstrap(instrumentation, args);
       
        return arthasBootstrap;
   
 进一步跟踪

    private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable
        this.instrumentation = instrumentation;
 
        //新版才加入的,不明白为啥加入fastjson
        initFastjson();
 
        // 1. initSpy() 其实就是加载java.arthas.SpyAPI的class对象
        initSpy();
        // 2. ArthasEnvironment,扣的Spring环境的源码
        initArthasEnvironment(args);
 
        //输出路径
        String outputPathStr = configure.getOutputPath();
        if (outputPathStr == null)
            outputPathStr = ArthasConstants.ARTHAS_OUTPUT;
       
        outputPath = new File(outputPathStr);
        outputPath.mkdirs();
 
        // 3. init logger
        loggerContext = LogUtil.initLooger(arthasEnvironment);
 
        // 4. 增强ClassLoader,初始化    
        //instrumentation.addTransformer(classLoaderInstrumentTransformer, true);
        enhanceClassLoader();
        // 5. init beans  ResultViewResolver HistoryManagerImpl 结果解析器与历史记录
        initBeans();
 
        // 6. start agent server
        // 顾名思义,绑定shellServer 创建http Telnet的链接
        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);
   
环境信息的源码,其实是Spring的源码

 关键的字节码增强初始化

    public TransformerManager(Instrumentation instrumentation)
        this.instrumentation = instrumentation;
 
        classFileTransformer = new ClassFileTransformer()
 
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                    ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
                for (ClassFileTransformer classFileTransformer : reTransformers)
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null)
                        classfileBuffer = transformResult;
                   
               
 
                for (ClassFileTransformer classFileTransformer : watchTransformers)
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null)
                        classfileBuffer = transformResult;
                   
               
 
                for (ClassFileTransformer classFileTransformer : traceTransformers)
                    byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                            protectionDomain, classfileBuffer);
                    if (transformResult != null)
                        classfileBuffer = transformResult;
                   
               
 
                return classfileBuffer;
           
 
        ;
        instrumentation.addTransformer(classFileTransformer, true);
   
 instrumentation.addTransformer(classFileTransformer, true);

至此结束,其实原理很简单:attach 然后初始化Telnet与http服务,通过addTransformer来动态字节码增强,client端连接上去,然后发指令。

3. arthas的原理
基于Instrumentation的产品,除了arthas,常用的还有

pinpoint、skywalking
这些非常有名气 的产品。Instrumentation非常关键的API

arthas的关键原理是Javaagent,有2种方式

1.在 JVM 启动的时加载 JDK5开始支持

       使用javaagent VM参数 java -javaagent:xxxagent.jar xxx,这种方式在 main 方法之前执行 agent 中的 premain 方法
       public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception


 2.在 JVM 启动后 Attach JDK6开始支持

       通过 Attach API 进行加载,在进程存在的时候,动态attach,这种方式会在 agent 加载以后执行 agentmain 方法
       public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception

且必须加上maven插件参数,其他方式代码管理同理

<manifestEntries>
    <Premain-Class>com.taobao.arthas.agent334.AgentBootstrap</Premain-Class>
    <Agent-Class>com.taobao.arthas.agent334.AgentBootstrap</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
总结
arthas其实原理很简单,使用Javaagent技术,其实debug模式也是使用这种技术。arthas增强了字节码,写了一些native方法获取jvm的堆等信息。从源码看,不知道为啥使用telnet协议,重度依赖netty termd,为啥不使用简单的HTTP协议,无状态降低依赖,而且方便前端图形化,admin端目前是telnet透传。估计设计之初就认为是敲命令吧,但是有没有联想能力,敲命令还是很费时,需要学习。

以上是关于arthas底层实现原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

arthas底层实现原理剖析

HashMap底层实现原理剖析

java多态理解和底层实现原理剖析

深入探究 Objective-C 对象的底层原理 | 文末福利不可错过

从底层原理深度剖析volatile关键字

JVM技术专题「源码专题」深入剖析JVM的Mutex锁的运行原理及源码实现(底层原理-防面试)