字节码编程之redefineClasses与retransformClasses

王大爷 2020年11月28日 1,027次浏览

redefineClass代码使用实例

import java.lang.instrument.Instrumentation; //導入方法依賴的package包/類public static void agentmain(String args,Instrumentation inst){
    try{
        System.err.println("傳進來的參數為"+args);
        File f = new File(args);
        byte[] targetClassFile = new byte[(int)f.length()];
        DataInputStream dis = new DataInputStream(new FileInputStream(f));
        dis.readFully(targetClassFile);
        dis.close();

        DynamicClassLoader myLoader = new DynamicClassLoader();
        Class targetClazz = myLoader.findClass(targetClassFile);
        System.err.println("目標class類全路徑為"+targetClazz.getName());
        ClassDefinition clazzDef = new ClassDefinition(Class.forName(targetClazz.getName()), targetClassFile);
        inst.redefineClasses(clazzDef);

        System.err.println("重新定義"+args+"完成!!");

    }catch(Exception e){
        e.printStackTrace();
    }
}

下面是redefineClasses的部分代码

void redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
  if (!errorOccurred) {
    getDefinitionClassMethodID = (*jnienv)->GetMethodID(    jnienv,                                                classDefClass,                                                "getDefinitionClass","()Ljava/lang/Class;");
    ......
  }
  if (!errorOccurred) {
    getDefinitionClassFileMethodID = (*jnienv)->GetMethodID(    jnienv,                                                    classDefClass,                                                    "getDefinitionClassFile",                                                    "()[B");
    .....
  }
  classDefs[i].klass = (*jnienv)->CallObjectMethod(jnienv, classDef, getDefinitionClassMethodID);
  ...          targetFiles[i] = (*jnienv)->CallObjectMethod(jnienv, classDef, getDefinitionClassFileMethodID);
  ....           classDefs[i].class_byte_count = (*jnienv)->GetArrayLength(jnienv, targetFiles[i]);
  if (!errorOccurred) {
    jvmtiError  errorCode = JVMTI_ERROR_NONE;
    errorCode = (*jvmtienv)->RedefineClasses(jvmtienv, numDefs, classDefs);
  }
}

获取调用传递过来的ClassDefinition 里的class对象和需要修改的字节码,最后调用了JVMTI 的RedefineClasses的方法

JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {//TODO: add locking  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);  VMThread::execute(&op);  return (op.check_error());} /* end RedefineClasses */

后是交给VM Thread去执行的,为什么要交给VM Thread,修改Class内容需要进入Safepoint的点的是stop-world的操作,而VM Thread的操作都会进入Safepoint的点

RedefineClasses 分为3部分,参考jvmtiRedefineClasses.cpp

第一是doit_prologue 这个被在JavaThread里调用主要是解析和校验传递过来的bytecode生成scrash_class

  • 挨个遍历要批量重定义的jvmtiClassDefinition
  • 然后读取新的字节码,如果有关注ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改
  • 字节码解析好,创建一个klassOop对象
  • 对比新老类,并要求如下:
  1. 父类是同一个
  2. 实现的接口数也要相同,并且是相同的接口
  3. 类访问符必须一致
  4. 字段数和字段名要一致
  5. 新增的方法必须是private static/final的
  6. 可以删除修改方法
  7. 对新类做字节码校验

第二是doit() 这是在VMThread里执行的主要是替换原来的class里的内容

  • 合并常量
  • 清除原来类上的断点
  • 清除原来类上的JIT优化
  • 将老的jmethodId更新到新类的jmethodid上
  • 将新类的常量池的hodler指向老的类
  • 将新类和老类的一些属性做交换,比如常量池,methods,内部类
  • 初始化新的vtable和itable
  • 交换annotation的method、field、parameter
  • 遍历所有当前类的子类,修改他们的vtable及itable

第三是doit_epilogue()

  • ·主要是释放前面的步骤内存