一上来就说原理还是不怎么合适的,先给大家讲下这个技术怎么用吧。但是这篇文章重点不是讲怎么用,所以我只讲个大概流程。
第一步:写个Agent类,获取Instrumentation对象
public class MyAgent {
private static Instrumentation mInstrumentation;
public static void agentmain(String agentArgs, Instrumentation inst) {
mInstrumentation = inst;
}
// 拿到Instrumentation对象后就可以利用ClassModifierTransformer来进行类的热替换了
public static void modifyClass(Class clazz){
ClassFileTransformer transformer = new ClassModifierTransformer();
mInstrumentation.addTransformer(transformer, true);
mInstrumentation.retransformClasses(new Class[]{clazz});
mInstrumentation.removeTransformer(transformer);
}
}
第二步:写个ClassFileTransformer,利用ASM/Javassist等工具进行字节码修改
public class ClassModifierTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 在这里利用Javassist等工具修改类的字节码,返回修改后类的字节数组
return null;
}
}
目前已经有很多文章讲具体使用方法了,大家可以搜索下,我这里先介绍两篇:
基于Java Instrument的Agent实现
谈谈Java Intrumentation和相关应用
热替换的核心就在于Instrumentation的两个方法:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
addTransformer 用来注册类的修改器;retransformClasses 会让类重新加载,从而使得注册的类修改器能够重新修改类的字节码。
下面让我们重点讲讲这两个方法的实现:
1. addTransformer
可见我们自己的实现的 ClassFileTransformer 被添加到了 TransformerManager中,让我们跟进去看看:
ClassFileTransformer对象这次被放入了TransformerManager的一个数组中。 OK,注册完毕,很简单对不对?下面我们再来看下稍微复杂点的retransformClasses 吧。
2.retransformClasses
这个方法的实现最终调用的是个Native方法。很多同学看到Native方法就头疼,不要急,Native方法也是人写的,不过是一段文本而已。我们来看下具体实现吧:
继续跟进 -->
retransformClasses 最后会调用到 jvmtiEnv.cpp中的 RetransformClasses
补充:Klass是一个抽象基类,它定义了一些接口(纯虚函数),由 InstanceKlass 继承并实现,两者结合可以描述一个java类的方法、字段、父类等信息。InstanceKlass 在jvm层面可以描述绝大部分java类。
上面这段代码主要干了两件事:
(1) 根据 java 层的Class对象,找到JVM层的类实例 InstanceKlass,并获取类的字节码,存放在class_definitions数组中。因为可以一次替换多个类,所以这里加了一个循环体,遍历每个要修改的类。
(2) 调用VMThread::execute(&op)
在获取了类的字节码之后,创建了一个 VM_RedefineClasses 的 vmop,然后通知VMThread进行处理。
在分析代码之前,先来看下比较重要的 VM_Operation。VM_Operation 是虚拟机级别的操作,这些操作包含了所有JVM的内置操作,例如GC、获取线程栈等等。这个类是所有这些操作的基类,该类定义在hotspot/src/share/vm/runtime/vmOperations.hpp 。
首先,该类定义了 Mode 和 VMOp_Type 两个枚举。第一个表示该操作的模式,第二个表示该操作的类型。事实上所有的类型都在文件开头的宏定义中写明了,这里我们只关心 RedefineClasses 这个类型。Mode包括这四种:
再来看下 VM_RedefineClasses
VM_RedefineClasses是VM_Operation的子类,实现了类转换的所有逻辑。该类定义和实现分别在hotspot/src/share/vm/prims/jvmtiRedefineClasses.hpp和hotspot/src/share/vm/prims/jvmtiRedefineClasses.cpp。
对于一个VM_Operation的子类,首先需要关心 evaluation_mode 函数。VM_RedefineClasses 类中找不到该函数,因此它是一个需要在 safepoint 阻塞的操作。
然后就是核心操作,即 doit_prologue、doit、doit_epilogue。代码比较复杂,本节先介绍doit_prologue的实现。我们先从注释上了解每个步骤做了什么
1.doit_prologue
在 doit_prologue 阶段,整个操作都是在Java线程中进行的,因此不会阻塞VMThread,也不会被计入safepoint的耗时。注意整个源码中 the_class 表示待替换的类,scratch_class表示新的类。
该阶段主要做的就是准备需要的字节码,包括解析字节码、类的链接、常量池合并、字节码校验等步骤。需要说明的是,如果业务代码中准备新的字节码时间比较长(前面提到的获取新字节码的回调也是在这里发生),这个阶段时间就会变长,但是不会阻塞JVM的核心线程。
然后,我们看下这部分是如何实现的
VMThread::execute(&op) 中会调用到 VM_RedefineClasses::doit_prologue,核心逻辑在 VM_RedefineClasses::load_new_class_versions()
由于代码较长,分为多个部分,第一部分如下
parse_stream() 这里又调用了KlassFactory::check_class_file_load_hook
看名字就知道是个hook方法,它会调用post_class_file_load_hook。
利用JvmtiClassFileLoadHookPoster来通知类修改器进行类的修改。进入 poster.post() 里面
消息发给所有的 jvmtienv , 最终的调用如下:
实际的消息处理者:
eventHandlerClassFileLoadHook在收到消息后,会调用transformClassFile
,继续跟进--->
这里会利用JNI调用 java 层InstrumentationImpl的transform,你看,我们又绕到Java层了。
transform 方法的调用如下:
看到这儿,大家还记得我们开始的时候,会将我们自定义的ClassFileTransformer对象注册到TransformerManager中吗?这里终于派上用场了,TransformerManager的transform()方法会遍历它的注册数组,调用每个ClassFileTransformer对象的transform()方法,并将我们修改后的类字节码返回,返回后的字节码最终又回到了上面JVM层的transformClassFile()中,并最终交还给给class_file_load_hook 消息的发送方。