在日常开发中,经常会运用到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。
挑选咱们需要attach的JVM,会判别本地目录$HOME/.arthas/
中是否存在Arthas对应版别的jar包,假如不存在,则下载并解压到指定目$HOME/.arthas/
。
最终经过java指令运转Arthas client,尝试连接Arthas server(127.0.0.1:3568)
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 server还没有发动(attach到运用JVM),所以抛了一个ConnectException反常
接着发动Arthas server,将其attach到指定的运用JVM
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。
长途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完结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
。
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-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);
- 首要经过ArthasClassLoader加载arthas-core的初始化类
ArthasBootstrap
- 经过反射调用的方式调用
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和运用JVM之间同享了JMX等底层API(由BootstrapClassLoader和ExtClassLoader加载的类),所以Arthas能够经过调用JDK的一些API获取运用JVM相应的运转时数据,比方dashboard/thread/mbean
等指令。可是关于增强型指令如watch/trace/tt
,Arthas会对运用代码注入一些代码,当被增强的运用代码履行时,会履行到Arthas注入的代码,然后完结功用的增强。可是由于ApplicationClassLoader
和ArthasClassLoader
加载器的类之间是不行见的,也便是说运用代码是不能直接调用Arthas代码的,会有ClassNotFoundException或者ClassCastException。
Arthas采用了一种很巧的计划,引进了一个arthas-spy模块,相当于在运用和Arthas之间架起了一座桥梁。arthas-spy模块中只有一个SpyAPI
类文件。ApyAPI
类由BoostrapClassLoader
加载,所以SpyAPI
关于ApplicationClassLoader
和ArthasClassLoader
都是可见的,运用经过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");
}
}
}
- 首要经过
BootstrapClassLoader
直接加载SpyAPI
类 - 假如加载失败(可能是CLASSPATH中找不SpyAPI),将arthas-spy.jar添加到运用的
BootstrapClassLoader
的查找途径中 - 依照类加载的双亲派遣准则,加载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);
}
}
由于AbstractSpy
是SpyAPI
的内部静态类,并且在SpyAPI
中界说了一个静态特点,所以AbstractApy
也会由BoostrapClassLoader
加载。而SpyImpl
在arthas-core模块中完结,所以会被ArthasClassLoader
加载。
所以Arthas能够经过调用SpyAPI
的setSpy
办法设置增强代码的详细履行逻辑。由于AbstractApy
由BoostrapClassLoader
加载,SpyImpl
由ArthasClassLoader
加载,所以SpyImpl
实例能够向上类型转换成AbstractApy
实例,完结赋值操作。
运用代码在调用Arthas的增强代码时,是经过调用SpyAPI
的静态办法,然后调用AbstractSpy
实例完结办法增强。
3. 怎样支撑OGNL?
Arthas是支撑OGNL表达式的,所以Arthas的时候能够十分灵活,例如下面检查第一个参数大于10的指令。
watch com.cm4j.loader.demo.Demo random '{params[0]}' 'params[0] > 10'
那它详细是怎样支撑的呢?经过断点咱们很简单能够定位到OGNL的处理逻辑:
Arthas会将增强履行的成果悉数放在Advice实例中,首要包含增强的办法名、参数、履行成果、耗时等数据,在回来是先判别是否有OGNL表达式,假如有OGNL表达式,会履行OGNL表达式,针对OGNL设置的条件进行过滤或者数值挑选。首要依赖了ognl对应的jar包。
4. 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会对相关办法进行3处增强:调用之前增强、函数回来之后增强、函数反常之后增强。
5. 参阅链接
- Arthas原理:怎样做到与运用代码阻隔
- Arthas原理:Arthas的指令分类及原理
- DebugArthasInIDEA
- javaattachapi