考拉性能测试总结(二)

Class Transform的实现

这里说的class transform其实是狭义的,主要是针对第一次类文件加载时就要求被transform的场景,在加载类文件的时候发出ClassFileLoad事件,然后交给instrumenat agent来调用javaagent里注册的ClassFileTransformer实现字节码的修改。


Class Redefine的实现

类重新定义,这是Instrumentation提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,要做这件事,我们必须要知道两个东西,一个是要修改哪个类,另外一个是想将那个类修改成怎样的结构,有了这两个信息之后就可以通过InstrumentationImpl下面的redefineClasses方法操作了:

public void redefineClasses(ClassDefinition[]   definitions) throws  ClassNotFoundException {

        if (!isRedefineClassesSupported()) {

            throw new UnsupportedOperationException("redefineClasses is not supported in this environment");

        }

        if (definitions == null) {

            throw new NullPointerException("null passed as 'definitions' in redefineClasses");

        }

        for (int i = 0; i < definitions.length; ++i) {

            if (definitions[i] == null) {

                throw new NullPointerException("element of 'definitions' is null in redefineClasses");

            }

        }

        if (definitions.length == 0) {

            return; // short-circuit if there are no changes requested

        }


        redefineClasses0(mNativeAgent, definitions);

    }

在JVM里对应的实现是创建一个VM_RedefineClassesVM_Operation,注意执行它的时候会stop-the-world:

jvmtiError

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 */

这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:

  • 挨个遍历要批量重定义的jvmtiClassDefinition
  • 然后读取新的字节码,如果有关注ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改
  • 字节码解析好,创建一个klassOop对象
  • 对比新老类,并要求如下:
    • 父类是同一个
    • 实现的接口数也要相同,并且是相同的接口
    • 类访问符必须一致
    • 字段数和字段名要一致
    • 新增的方法必须是private static/final的
    • 可以删除修改方法
  • 对新类做字节码校验
  • 合并新老类的常量池
  • 如果老类上有断点,那都清除掉
  • 对老类做JIT去优化
  • 对新老方法匹配的方法的jmethodId做更新,将老的jmethodId更新到新的method上
  • 新类的常量池的holer指向老的类
  • 将新类和老类的一些属性做交换,比如常量池,methods,内部类
  • 初始化新的vtable和itable
  • 交换annotation的method、field、paramenter
  • 遍历所有当前类的子类,修改他们的vtable及itable

上面是基本的过程,总的来说就是只更新了类里的内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新所带来的开销。


Class Retransform的实现

retransform class可以简单理解为回滚操作,具体回滚到哪个版本,这个需要看情况而定,下面不管那种情况都有一个前提,那就是javaagent已经要求要有retransform的能力了:

  • 如果类是在第一次加载的的时候就做了transform,那么做retransform的时候会将代码回滚到transform之后的代码
  • 如果类是在第一次加载的的时候没有任何变化,那么做retransform的时候会将代码回滚到最原始的类文件里的字节码
  • 如果类已经加载了,期间类可能做过多次redefine(比如被另外一个agent做过),但是接下来加载一个新的agent要求有retransform的能力了,然后对类做redefine的动作,那么retransform的时候会将代码回滚到上一个agent最后一次做redefine后的字节码

我们从InstrumentationImpl的retransformClasses方法参数看猜到应该是做回滚操作,因为我们只指定了class:

    public void retransformClasses(Class<?>[] classes) {

        if (!isRetransformClassesSupported()) {

            throw new UnsupportedOperationException( "retransformClasses is not supported in this environment");

        }

        retransformClasses0(mNativeAgent, classes);

    }

不过retransform的实现其实也是通过redefine的功能来实现,在类加载的时候有比较小的差别,主要体现在究竟会走哪些transform上,如果当前是做retransform的话,那将忽略那些注册到正常的TransformerManager里的ClassFileTransformer,而只会走专门为retransform而准备的TransformerManager的ClassFileTransformer,不然想象一下字节码又被无声无息改成某个中间态了。

private:

  void post_all_envs() {

    if (_load_kind != jvmti_class_load_kind_retransform) {

      // for class load and redefine,

      // call the non-retransformable agents

      JvmtiEnvIterator it;

      for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {

        if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {

          // non-retransformable agents cannot retransform back,

          // so no need to cache the original class file bytes

          post_to_env(env, false);

        }

      }

    }

    JvmtiEnvIterator it;

    for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {

      // retransformable agents get all events

      if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {

        // retransformable agents need to cache the original class file

        // bytes if changes are made via the ClassFileLoadHook

        post_to_env(env, true);

      }

    }

  }


javaagent的其他小众功能

javaagent除了做字节码上面的修改之外,其实还有一些小功能,有时候还是挺有用的

  • 获取所有已经被加载的类:Class[] getAllLoadedClasses(); 
  • 获取所有已经初始化了的类: Class[] getInitiatedClasses(ClassLoader loader); 
  • 获取某个对象的大小: long getObjectSize(Object objectToSize); 
  • 将某个jar加入到bootstrap classpath里优先其他jar被加载: void appendToBootstrapClassLoaderSearch(JarFile jarfile); 
  • 将某个jar加入到classpath里供appclassloard去加载:void appendToSystemClassLoaderSearch(JarFile jarfile); 
  • 设置某些native方法的前缀,主要在找native方法的时候做规则匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。

本文来自网易实践者社区,经作者张子铎授权发布。