【问题标题】:How to speed up runtime Java code instrumentation?如何加快运行时 Java 代码检测?
【发布时间】:2020-07-04 16:04:38
【问题描述】:

我制作了一个 Java 代理,它在 运行时 期间附加到 JVM,并检测所有加载的项目类并插入一些日志记录语句。总共有 11k 个类。我测量了我的ClassFileTransformertransform 方法所花费的总时间,它是3 秒。但是整个检测过程的持续时间大约需要 30 秒。 这就是我重新转换我的课程的方式:

 instrumentation.retransformClasses(myClassesArray);

我假设 JVM 占用了大部分时间来重新加载更改的类。是对的吗?如何加快检测过程?

更新
当我的代理被附加时,

instrumentation.addTransformer(new MyTransfomer(), true);
instrumentation.retransformClasses(retransformClassArray);

只被调用一次

然后MyTransfomer 类检测类并测量检测的总持续时间:


public class MyTransfomer implements ClassFileTransformer {
private long total = 0;
private long min = ..., max = ...;

public final byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
   long s = System.currentTimeMillis();
   if(s < min) min = s;
   if(s > max) max = s;
   byte[] transformed = this.transformInner(loader, className, classFileBuffer);

   this.total += System.currentTimeMillis() - s;
   
   return transformed;
  }
}

检测所有类后(从初始数组)(全局缓存跟踪检测的类)total 被打印出来,大约需要 3 秒。但是max-min 大约是 30 秒。

更新 2:

查看堆栈跟踪后会发生以下情况: 我打电话

instrumentation.retransformClasses(retransformClassArray);

调用本机方法retransformClasses0()。一段时间后(!)JVM调用sun.instrument.InstrumentationImpl类的transform()方法(但是这个方法一次只需要一个类,所以JVM连续多次调用这个方法),它调用transform() sun.instrument.TransformerManager 对象有一个列表,其中包含所有已注册的 ClassTransformers 并调用这些转换器中的每一个来转换类(我只注册了一个转换器!!)。

所以在我看来,大部分时间都花在了 JVM 上(在调用 retransformClasses0() 之后和每次调用 sun.instrument.InstrumentationImpl.transform() 之前)。有没有办法减少 JVM 执行此任务所需的时间?

【问题讨论】:

  • 如果没有源代码、java 版本和类路径,几乎是不可能提供帮助的。你能把它添加到问题中或创建一个 github 项目吗?
  • @Jeff 更新了代码。
  • 真的需要看看,如果可能的话,transformInner 在做什么。另外我建议记录每个类的执行时间,看看是否有特定的类有问题。
  • JVM中是否注册了其他类文件转换器?
  • @Holger,没有

标签: java instrumentation javaagents


【解决方案1】:

更正:

因为retransformClasses(classArr) 不会立即重新转换 classArr 中的所有元素,而是会根据需要重新转换每个元素(例如,在链接时)。(请参阅 jdk [VM_RedefineClasses][1] 和 [jvmtiEnv][ 2]),它会立即重新转换所有这些。

retransformClasses() 的作用:

  1. 将控制权转移到原生层,并给它一个我们想要转换的类列表
  2. 对于每个要转换的类,本机代码会尝试通过调用我们的 Java 转换器来获取新版本,这会导致 Java 代码和本机代码之间的控制转移。
  3. 本机代码将内部表示的适当部分替换为给定的新类版本。

在第 1 步中:

java.lang.instrument.Instrumentation#retransformClasses调用sun.instrument.InstrumentationImpl#retransformClasses0是一个JNI方法,控制权会转移到native层。

// src/hotspot/share/prims/jvmtiEnv.cpp
jvmtiError
JvmtiEnv::RetransformClasses(jint class_count, const jclass* classes) {
  ...
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_retransform);
  VMThread::execute(&op);
  ...
} /* end RetransformClasses */

在第 2 步中:

这一步由KlassFactory::create_from_stream实现,这个过程会发布一个ClassFileLoadHook事件,其回调可以通过调用java转换器方法获取转换后的字节码。在这一步,控件会在native代码和java代码之间来回切换。

// src/hotspot/share/classfile/klassFactory.cpp
// check and post a ClassFileLoadHook event before loading a class
// Skip this processing for VM hidden or anonymous classes
if (!cl_info.is_hidden() && (cl_info.unsafe_anonymous_host() == NULL)) {
  stream = check_class_file_load_hook(stream,
                                      name,
                                      loader_data,
                                      cl_info.protection_domain(),
                                      &cached_class_file,
                                      CHECK_NULL);
}
//src/java.instrument/share/native/libinstrument/JPLISAgent.c :
//call java code sun.instrument.InstrumentationImpl#transform
transformedBufferObject = (*jnienv)->CallObjectMethod(
   jnienv,
   agent->mInstrumentationImpl, //sun.instrument.InstrumentationImpl
   agent->mTransform, //transform
   moduleObject,
   loaderObject,
   classNameStringObject,
   classBeingRedefined,
   protectionDomain,
   classFileBufferObject,
   is_retransformer);

在第 3 步中:

VM_RedefineClasses::redefine_single_class(jclass the_jclass, InstanceKlass* scratch_class, TRAPS) 方法将目标类中的部分(如常量池、方法等)替换为转换后的类中的部分。

// src/hotspot/share/prims/jvmtiRedefineClasses.cpp
for (int i = 0; i < _class_count; i++) {
  redefine_single_class(_class_defs[i].klass, _scratch_classes[i], thread);
}

那么如何加快运行时 Java 代码检测?

在我的项目中,如果应用在转换时处于暂停状态,total 时间和max-min 时间几乎相同。你能提供一些演示代码吗?

改变 jvm 的工作方式是不可能的,所以多线程可能不是一个坏主意。在我的演示项目中使用多线程后,它的速度提高了好几倍。

【讨论】:

  • 如果可以的话,我会给你更多的分数。这个答案真的很到位。谢谢!
  • @Nfff3 是吗?此答案描述了转换过程中执行的一些步骤,并建议对您的代理进行更改,该更改不会提高速度,而只会更改您正在测量的内容。它如何解决您的问题?我做了一些实验,在我的设置中,11k 个类在不到一秒的时间内完成了转换。那么为什么你的情况这么慢,你是如何解决的呢?如果这个答案说的话,它一定是隐藏得很好。
  • 我在做原生代理,转换过程有点复杂。如果您需要,我可以提供一个简单的 POC 项目来提供帮助。您可以参考docs.oracle.com/javase/specs/jvms/se14/html/jvms-5.html 以获取有关“加载、链接和初始化”的更多信息。我稍后会补充这个答案。
  • 奇怪你的转换过程这么慢,你试试我的jdk forkgithub.com/rieonke/jdk/tree/agent_measure,转换的时候会打印一些日志,M_CALL_INSTRU_IMPL是指sun.instrument.InstrumentationImpl#transform花费的时间,而 M_LOAD_NEW_VERSION 是 step2 花费的总时间。您可以将其与您自己的测量值进行比较。
  • @Nfff3 我认为,这在很大程度上取决于已经使用了多少类以及如何使用。换句话说,应用更改需要多少去优化。
【解决方案2】:

从您的描述看来,整个转换似乎是在单个线程中运行的。

您可以创建多个线程,每个线程同时转换一个类。作为一个类的转换应该独立于任何其他类。这应该可以让您在整体转换时间上缩短执行系统上可用的已用 Core 数量。

您可以使用以下方法计算核心数:

int cores = Runtime.getRuntime().availableProcessors();

将要转换为核心数量的类列表分块,并创建可能的线程以并行处理这些块。

【讨论】:

  • 我认为这不是问题所在。请参阅我的第二次更新。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-25
  • 1970-01-01
  • 2021-02-25
相关资源
最近更新 更多