在日常开发中,经常会运用到arthas排查线上问题,觉得arthas的功用十分强大,所以计划花了点时刻了解一下其完结原理。并试着回答一下运用Arthas时存在的一些疑问。

Arthas首要基所以Instrumentation + JavaAgent + Attach API + ASM + 反射 + OGNL等技术完结的。在不停止运用服务的情况下,将Arthas的jar包代码动态加载到运用的JVM中,再合作Instrumentation类,动态修正运用JVM中运营的字节码,完结对方针运用增强,如获取某办法的参数、回来值、耗时等信息、调用JVM相关类获取JVM运转时信息,最终再经过OGNL过滤、存取目标特点。

1. 怎样attach到运用?

1.1 怎样debug?

Arthas的发动很简单,从github上把Arthas的代码clone到本地,然后直接运转/bin目录下的as.sh脚本便能发动。为了弄明白Arthas attach到运用的进程,能够加上–debug-attach参数,一起为了检查脚本的详细履行流程,bash加上-x选项。

bash -x ./as.sh --debug-attach

依据打印的履行流程,开端首要是进行一些装备检查、目录创建和运转参数的构造。前置作业准备好后,会调用jps指令列出体系当时一切运转的JVM。

Arthas源码分析

挑选咱们需要attach的JVM,会判别本地目录$HOME/.arthas/中是否存在Arthas对应版别的jar包,假如不存在,则下载并解压到指定目$HOME/.arthas/

Arthas源码分析

Arthas源码分析

最终经过java指令运转Arthas client,尝试连接Arthas server(127.0.0.1:3568)

Arthas源码分析

java -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-client.jar 127.0.0.1 3658 -c session --execution-timeout 2000

从发动的指令能够看到,首要是运转了arthas-client.jar包,一起敞开了长途debug,长途debug端口号8888,由于设置了suspend=y,发动流程被阻塞,等候debugger attach。翻开IDEA,装备长途debug,然后点击debug,流程即可继续。

Arthas源码分析

由于当时Arthas server还没有发动(attach到运用JVM),所以抛了一个ConnectException反常

Arthas源码分析

接着发动Arthas server,将其attach到指定的运用JVM

Arthas源码分析

java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/lib/tools.jar -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -pid 57840 -core /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -agent /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-agent.jar

从发动的指令能够看到,首要是运转了arthas-core.jar和arthas-agent.jar两个jar包,一起敞开了长途debug,长途debug端口号8888。检查arthas-core和arthas-agent两个模块下的pom文件,能够发现main办法在arthas-core模块下的com.taobao.arthas.core.Arthas类中,所以在arthas-core模块的main办法设置断点,然后点击debug,即可开端attach进程的debug。

Arthas源码分析

长途debug连接成功之后,attach流程就很简单弄明白了。attach的进程首要是在attachAgent中完结的。

1.2 Arthas attach到运用JVM

attach是运用sun供给JVM Attach API完结的。中心代码如下:

VirtualMachineDescriptor virtualMachineDescriptor = null;
// 1. 列出当时体系运转的一切JVM,和jps类似
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
    String pid = descriptor.id();
    // 找到指定PID对应的JVM
    if (pid.equals(Long.toString(configure.getJavaPid()))) {
        virtualMachineDescriptor = descriptor;
        break;
    }
}
// attach到指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
// 指定agent jar包和相关装备
virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());

Arthas源码分析

至此,Arthas完结attach到方针运用JVM的进程。

2. Arthas与attach运用阻隔

Arthas的代码attach到运用对应的JVM后,将由运用JVM加载运转,为了防止Arthas代码对运用的影响,Arthas进行了代码阻隔。在介绍代码阻隔的详细完结之前,先看一下怎样进行debug。由于attach完结之后,Arthas的代码是由运用JVM进行加载和运转的,所以需要运用代码中进行debug。可是运用中并没有引进Arthas的jar包,无法直接进行debug。能够参阅attach进程的debug,在发动运用的时候敞开长途debug,然后在Arthas源码中进行debug。

2.1 在运用JVM中debug Arthas

直接凭借官网的例子,假设运用代码如下:

package com.banzhe.loader.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo {
    static class Counter {
        private static final AtomicInteger count = new AtomicInteger(0);
        public static void increment() {
            count.incrementAndGet();
        }
        public static int value() {
            return count.get();
        }
    }
    public static void main(String[] args) {
        while (true) {
            Counter.increment();
            try {
                System.out.println("counter: " + Counter.value());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ignored) {
                break;
            }
        }
    }
}

以支撑长途debug的方式发动,debug端口为:8000

# 编译
javac com/banzhe/loader/demo/*.java
# 运转
java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n com.banzhe.loader.demo.Demo

由于Arthas是在运用运转时attach到运用JVM的,attach完结之后,运用JVM会以Agent-Class的agentmain办法作为进口办法履行。所以在agentmain办法中打断点,然后运转bash ./as.sh发动Arthas即可。从arthas-agent模块的pom文件中可知道Agent-Class为com.taobao.arthas.agent334.AgentBootstrap

Arthas源码分析

2.2 类阻隔

为了防止Arthas代码对运用的影响,Arthas进行了代码阻隔。在JVM中一个类型实例是经过类加载器+全类名确认的。也便是说为了防止不同模块代码间相互影响(两个jar中可能会存在全类名相同,可是逻辑完全不同的类),能够经过运用不同的ClassLoader进行加载来完结阻隔。如pandora boot、tomcat等都是依据ClassLoader完结代码阻隔的,Arthas也是经过界说了自己的ClassLoader——ArthasClassLoader来完结与运用代码阻隔的。

public class ArthasClassloader extends URLClassLoader {
    public ArthasClassloader(URL[] urls) {
        super(urls, ClassLoader.getSystemClassLoader().getParent());
    }
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }
        // 优先从parent(SystemClassLoader)里加载体系类,防止抛出ClassNotFoundException
        if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
            return super.loadClass(name, resolve);
        }
        try {
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
            // ignore
        }
        return super.loadClass(name, resolve);
    }
}

Arthas源码分析

能够发现arthas-core.jar中的类都是由ArthasClassLoader加载的。

由于类加载有一个准则:

加载当时类的加载器,也会用于加载其所依赖的类(当然不一定是它加载,也可能遵循双亲派遣准则,由双亲加载器加载)

由于Agent-Class——com.taobao.arthas.agent334.AgentBootstrap的agentmain是agent的进口办法,attach完结之后由运用JVM加载,一般是ApplicationClassLoader加载器加载。依照上述类加载器的准则,Arthas的代码也都会由运用的ApplicationClassLoader加载,无法完结代码阻隔。所以Arthas经过反射打破了这个规则,完结Arthas代码与运用代码的阻隔。

// 运用ArthasClassLoader加载arthas-core的初始化类
Class<?> bootstrapClass = agentLoader.loadClass(""com.taobao.arthas.core.server.ArthasBootstrap"");
// 经过反射调用初始化函数
Object bootstrap = bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);
  1. 首要经过ArthasClassLoader加载arthas-core的初始化类ArthasBootstrap
  2. 经过反射调用的方式调用getInstance完结Arthas server端的初始化

由于ArthasBootstrap是由ArthasClassLoader加载器加载的,ArthasBootstrap担任初始化arthas-core,依照加载当时类的加载器,也会用于加载其所依赖的类准则,ArthasBootstrap依赖的类也会由ArthasClassLoader加载,所以便是完结了Arthas与运用的阻隔。

注意:不能将反射调用成果强制转换成ArthasBootstrap,否则会抛ClassCastException。由于左边的ArthasBootstrap的class实例是由运用的ApplicationClassLoader加载的,而右边的ArthasBootstrap的class实例是由ArthasClassLoader加载的,class实例不同不能进行转换。

ArthasBootstrap bootstrap = (ArthasBootstrap) bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);

2.3 运用代码调用arthas代码

依据上一节咱们能够知道,Arthas的代码和运用的代码是经过类加载器阻隔的,其类加载器结构如下图。

Arthas源码分析

依据类加载器的双亲派遣准则,父类加载器加载的类对子类加载器是可见的,而子类加载器加载的类对父类加载器是不行见的,兄弟类加载器加载的类互相是不行见的。也便是说Arthas和运用JVM之间同享了JMX等底层API(由BootstrapClassLoader和ExtClassLoader加载的类),所以Arthas能够经过调用JDK的一些API获取运用JVM相应的运转时数据,比方dashboard/thread/mbean等指令。可是关于增强型指令如watch/trace/tt,Arthas会对运用代码注入一些代码,当被增强的运用代码履行时,会履行到Arthas注入的代码,然后完结功用的增强。可是由于ApplicationClassLoaderArthasClassLoader加载器的类之间是不行见的,也便是说运用代码是不能直接调用Arthas代码的,会有ClassNotFoundException或者ClassCastException。

Arthas采用了一种很巧的计划,引进了一个arthas-spy模块,相当于在运用和Arthas之间架起了一座桥梁。arthas-spy模块中只有一个SpyAPI类文件。ApyAPI类由BoostrapClassLoader加载,所以SpyAPI关于ApplicationClassLoaderArthasClassLoader都是可见的,运用经过SpyAPI完结对Arthas的调用,然后完结功用的增强。

下面咱们经过代码来看一下详细是怎样完结的:

private void initSpy() throws Throwable {
    // TODO init SpyImpl ?
    // 将Spy添加到BootstrapClassLoader
    ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
    Class<?> spyClass = null;
    if (parent != null) {
        try {
            spyClass =parent.loadClass("java.arthas.SpyAPI");
        } catch (Throwable e) {
            // ignore
        }
    }
    if (spyClass == null) {
        CodeSource codeSource = ArthasBootstrap.class.getProtectionDomain().getCodeSource();
        if (codeSource != null) {
            File arthasCoreJarFile = new File(codeSource.getLocation().toURI().getSchemeSpecificPart());
            File spyJarFile = new File(arthasCoreJarFile.getParentFile(), "arthas-spy.jar");
            instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
        } else {
            throw new IllegalStateException("can not find " + "arthas-spy.jar");
        }
    }
}
  1. 首要经过BootstrapClassLoader直接加载SpyAPI
  2. 假如加载失败(可能是CLASSPATH中找不SpyAPI),将arthas-spy.jar添加到运用的BootstrapClassLoader的查找途径中
  3. 依照类加载的双亲派遣准则,加载SpyAPI类时,会优先托付给BootstrapClassLoader,所以SpyAPI会被根类加载器加载,而不是ArthasClassLoader加载

SpyAPI中界说了不一起机的静态增强处理函数,详细的处理逻辑由抽象类AbstractSpy的子类SpyImpl完结。

public class SpyAPI {
    private static volatile AbstractSpy spyInstance = NOPSPY;
    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);
    }
    public static void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
            Object returnObject) {
        spyInstance.atExit(clazz, methodInfo, target, args, returnObject);
    }
    public static void atExceptionExit(Class<?> clazz, String methodInfo, Object target,
            Object[] args, Throwable throwable) {
        spyInstance.atExceptionExit(clazz, methodInfo, target, args, throwable);
    }
    public static void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target) {
        spyInstance.atBeforeInvoke(clazz, invokeInfo, target);
    }
    public static void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target) {
        spyInstance.atAfterInvoke(clazz, invokeInfo, target);
    }
    public static void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable) {
        spyInstance.atInvokeException(clazz, invokeInfo, target, throwable);
    }
    public static abstract class AbstractSpy {
        public abstract void atEnter(Class<?> clazz, String methodInfo, Object target,
                Object[] args);
        public abstract void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,
                Object returnObject);
        public abstract void atExceptionExit(Class<?> clazz, String methodInfo, Object target,
                Object[] args, Throwable throwable);
        public abstract void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target);
        public abstract void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target);
        public abstract void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable);
    }
}

由于AbstractSpySpyAPI的内部静态类,并且在SpyAPI中界说了一个静态特点,所以AbstractApy也会由BoostrapClassLoader加载。而SpyImpl在arthas-core模块中完结,所以会被ArthasClassLoader加载。

所以Arthas能够经过调用SpyAPIsetSpy办法设置增强代码的详细履行逻辑。由于AbstractApyBoostrapClassLoader加载,SpyImplArthasClassLoader加载,所以SpyImpl实例能够向上类型转换成AbstractApy实例,完结赋值操作。

运用代码在调用Arthas的增强代码时,是经过调用SpyAPI的静态办法,然后调用AbstractSpy实例完结办法增强。

Arthas源码分析

3. 怎样支撑OGNL?

Arthas是支撑OGNL表达式的,所以Arthas的时候能够十分灵活,例如下面检查第一个参数大于10的指令。

watch com.cm4j.loader.demo.Demo random '{params[0]}' 'params[0] > 10'

那它详细是怎样支撑的呢?经过断点咱们很简单能够定位到OGNL的处理逻辑:

Arthas源码分析

Arthas会将增强履行的成果悉数放在Advice实例中,首要包含增强的办法名、参数、履行成果、耗时等数据,在回来是先判别是否有OGNL表达式,假如有OGNL表达式,会履行OGNL表达式,针对OGNL设置的条件进行过滤或者数值挑选。首要依赖了ognl对应的jar包。

Arthas源码分析

4. Arthas指令分类及原理

Arthas源码分析

下面首要介绍一下watch/trace/tt等增强指令的完结的原理。Arthas的字节码增强是依据bytekit完结,bytekit是对ASM的封装,依据供给更高层的字节码处理能力,面向确诊/APM领域的字节码库,一起供给一套简洁的API,让开发人员能够很轻松的完结字节码增强。例如下面临Sample类的hello办法进行增强,打印hello办法的耗时:

public class ByteKitDemo {
    public static class Sample {
        public void hello(String name) {
            System.out.println("Hello " + name + "!");
        }
    }
    public static class SampleInterceptor {
        private static long start;
        @AtEnter(inline = true)
        public static void atEnter() {
            start = System.currentTimeMillis();
        }
        @AtExit(inline = true)
        public static void atEit() {
            System.out.println(System.currentTimeMillis() - start);
        }
    }
    public static void main(String[] args) throws Exception {
        // 解析界说的 Interceptor类 和相关的注解
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class);
        // 加载字节码
        ClassNode classNode = AsmUtils.loadClass(Sample.class);
        // 对加载到的字节码做增强处理
        for (MethodNode methodNode : classNode.methods) {
            MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
            for (InterceptorProcessor interceptor : processors) {
                interceptor.process(methodProcessor);
            }
        }
        // 获取增强后的字节码
        byte[] bytes = AsmUtils.toBytes(classNode);
        // 检查反编译成果
        System.out.println(Decompiler.decompile(bytes));
    }
}

履行成果:

public static class ByteKitDemo.Sample {
    public ByteKitDemo.Sample() {
        ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis();
        System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start);
    }
    /*
     * WARNING - void declaration
     */
    public void hello(String string) {
        void name;
        ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis();
        System.out.println("Hello " + (String)name + "!");
        System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start);
    }
}

能够发现整个增强完结的代码可读性是十分好的。

Arthas的watch指令增强中心代码如下:

// SpyInterceptor1对应函数调用之前增强
interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class));
// SpyInterceptor2对应函数回来之后增强
interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class));
// SpyInterceptor3对应函数反常之后增强
interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class));

一切增强的完结都在SpyImpl中完结,Arthas会将办法名、参数、回来值等信息一致保存到Advice中,然后在依照对应格局进行处理回来。

能够验证一下,运用watch指令时,arthas会对相关的办法进行字节码增强。首要翻开一个arthas终端履行:

watch com.cm4j.loader.demo.Demo random  -n 1000

然后翻开另一个arthas终端,检查最新加载的Demo类

jad com.cm4j.loader.demo.Demo

Arthas源码分析

能够发现,arthas会对相关办法进行3处增强:调用之前增强、函数回来之后增强、函数反常之后增强。

5. 参阅链接

  1. Arthas原理:怎样做到与运用代码阻隔
  2. Arthas原理:Arthas的指令分类及原理
  3. DebugArthasInIDEA
  4. javaattachapi