Java Instrumentation

我们在项目调试、排查问题、性能监控的时候经常会用到一些开源工具,比如btrace、jprofile、arthas。今天这篇文章就带你了解下这些工具背后的实现原理:Java Instrumentation


1、Java Instrumentation

Instrument是jvm 提供的一个可以修改已加载类的类库,专门为java语言提供的插桩服务提供支持。
使用 Instrumentation,可以替换和修改某些类的定义、向classLoader的classpath下加入jar文件等。我们构建一个独立于应用程序的代理程序(Agent),进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。

Instrumentation 类有常用的方法:

方法 说明
void addTransformer(ClassFileTransformer transformer) 注册提供的转换器
long getObjectSize(Object objectToSize) 返回指定类的大小
void retransformClasses(Class<?>… classes) 重转换提供的类集
Class[] getAllLoadedClasses() 获取当前加载的所有类
void appendToBootstrapClassLoaderSearch(JarFile jarfile) 将一个jar加入到bootstrap classloader的 classpath里
removeTransformer 移除转换器
redefineClasses 重定义class,会stw

我们常用的是 addTransformer 方法,它会注册一个ClassFileTransformer,在ClassFileTransformer 里可以直接对指定类的类字节码进行修改。通常来说,一般使用ASM,CGLIB,Byte Buddy,Javassist等框架进行字节码增强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

/**
* 实现一个自己的 ClassFileTransformer
* 使用Javassist给指定的类和方法添加执行时间
*/
@Slf4j
public class MyTransformer implements ClassFileTransformer {
private String targetClassName;
private ClassLoader targetClassLoader;
public AtmTransformer(String name, ClassLoader classLoader) {
this.targetClassName = name;
this.targetClassLoader = classLoader;
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
byte[] byteCode = classfileBuffer;
String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/");
if (!className.equals(finalTargetClassName)) {
return byteCode;
}

if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {

log.info("[Agent] Transforming class");
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod m = cc.getDeclaredMethod(TARGET_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;");

endBlock.append("LOGGER.info(\"[Application] operation completed in:" + "\" + opTime + \" ms!\");");

m.insertAfter(endBlock.toString());

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

2、Java Agent

上面的例子在ClassFileTransformer 里实现对字节码的增强,此时我们需要通过Java Agent 拿到一个Instrumentation实例,来实现增强的功能。

Java Agent是一种特殊的jar文件,它是Instrumentation的客户端。根据不同的启动时机,Agent类需要实现不同的方法(二选一)。

1
2
3
4
//静态加载
public static void premain(String agentArgs, Instrumentation inst);
//动态加载
public static void agentmain(String agentArgs, Instrumentation inst);

方法 说明
pemain 静态加载 以vm参数的形式载入。在程序main方法执行之前执行。其jar包的manifest需要配置属性Premain-Class。如果premain方法执行失败或者抛出异常,那么启动就会失败。
agentmain 动态加载 以Attach的方式载入,在Java程序启动后执行。其jar包的manifest需要配置属性Agent-Class。如果执行失败或者抛出异常,那么jvm会忽略错误,不会影响程序的正常运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 实现一个自己的 Agent
*/
@Sl4j
public class AgentTest {
public static void premain(String agentArgs, Instrumentation inst) {
log.info("[Agent] In premain method");
String className = "com.example.test.Base";
transformClass(className, inst);
}

public static void agentmain(String agentArgs, Instrumentation inst) {
log.info("[Agent] In agentmain method");
String className = "com.example.test.Base";
transformClass(className, inst);
}

private static void transformClass(String className, Instrumentation instrumentation) {
Class<?> targetCls = null;
ClassLoader targetClassLoader = null;
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
} catch (Exception ex) {
LOGGER.error("Class [{}] not found with Class.forName");
}

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) {
MyTransformer dt = new MyTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true); //增加自己的ClassFileTransformer
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Transform failed for: [" + clazz.getName() + "]", ex);
}
}
}

manifest配置则可以参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.test.AgentTest</Premain-Class>
<Agent-Class>com.example.test.AgentTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>

按照上述步骤实现了Agent后,需要打出Agent jar包。然后看你是需要静态加载还是动态加载。

  • 静态加载:使用vm参数(如java -javaagent:agent.jar -jar application.jar)跟随宿主程序一起启动
  • 动态加载:使用 jvm attach机制启动

3、Attach机制

1
2
3
4
5
6
7
//使用例子
VirtualMachine vm = VirtualMachine.attach("52341"); //JVM进程pid
try {
vm.loadAgent(".../agent.jar"); // 指定agent的jar包路径,发送给目标进程
} finally {
vm.detach();
}

VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是与target VM搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。

上述代码里在vm.loadAgent之后,相应的agent就会被目标JVM进程加载,并执行agentmain方法。

Attach API不仅仅可以实现动态加载agent,它其实是跨JVM进程通讯的工具,能够将某种指令从一个JVM进程发送给另一个JVM进程。加载agent只是Attach API发送的各种指令中的一种, 诸如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能,都属于Attach API可以发送的指令。
这里有一篇详细讲述 java attach 的文章,值得阅读下。

4、JVMTI

JVMTI =JVM Tool Interface是JVM提供的一套对JVM进行操作的native编程接口。JVMTI是实现Java调试器,以及其它Java运行态测试与分析工具的基础。

通过JVMTI可以实现对JVM的多种操作,然后通过接口注册各种事件勾子。在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等

javaagent 也是基于JVMTI实现,借助JVMTI的一部分能力,帮助动态重载类信息
c35d6d8a-d2ab-11e9-97ed-0a58ac13050a.jpeg

5、相关技术的实际应用

Arthas 是Alibaba开源的Java诊断工具。提供了非常强大功能。Arthas能做什么?Arthas官方文档里面是这样说的:

1
2
3
4
5
6
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到JVM的实时运行状态?

除了官方文档里面提到的这几点,它还能对方法的入参、出参进行监控,记录方法内部调用路径,执行耗时,还有死锁、jar包冲突、占用CUP过大的线程分析等问题。

这是Arthas的一张说明图。
8a8251ac-d2b9-11e9-8968-0a58ac1314b0.png

可以看出就是对Instrumentation的实际应用。一些技术细节 例如类加载的隔离值得研究学习。
这个项目已经在github开源,相关文档和教程也很丰富(Arthas

0%