如何加速运行时 Java 代码检测?

Nff*_*ff3 6 java instrumentation javaagents

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

 instrumentation.retransformClasses(myClassesArray);
Run Code Online (Sandbox Code Playgroud)

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

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

 instrumentation.retransformClasses(myClassesArray);
Run Code Online (Sandbox Code Playgroud)

只调用一次

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

instrumentation.addTransformer(new MyTransfomer(), true);
instrumentation.retransformClasses(retransformClassArray);
Run Code Online (Sandbox Code Playgroud)

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

更新 2:

查看堆栈跟踪后,发生了以下情况:我调用

instrumentation.retransformClasses(retransformClassArray);
Run Code Online (Sandbox Code Playgroud)

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

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

Rie*_* Ke 2

更正:

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

retransformClasses() 的作用是:

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

在步骤 1 中:

java.lang.instrument.Instrumentation#retransformClasses调用sun.instrument.InstrumentationImpl#retransformClasses0JNI方法,控制权将转移到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 */

Run Code Online (Sandbox Code Playgroud)

在步骤 2 中:

该步骤由 实现KlassFactory::create_from_stream,该过程将发布一个ClassFileLoadHook事件,该事件的回调可以通过调用java的transformer方法来获取转换后的字节码。在这一步中,控件将在本机代码和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);
}
Run Code Online (Sandbox Code Playgroud)
//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);
Run Code Online (Sandbox Code Playgroud)

在步骤 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);
}
Run Code Online (Sandbox Code Playgroud)

那么如何加快 Java 代码检测的运行速度呢?

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

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

  • 我正在开发一个本地代理,转换过程有点复杂。如果您需要,我可以提供一个简单的 POC 项目来帮助您。您可以参考 https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-5.html 获取有关“加载、链接和初始化”的更多信息。稍后我会补充这个答案。 (2认同)
  • 奇怪的是你的转换过程这么慢,你试试我的jdk fork https://github.com/rieonke/jdk/tree/agent_measure,它在转换时会打印一些日志,M_CALL_INSTRU_IMPL表示sun.instrument花费的时间.InstrumentationImpl#transform,M_LOAD_NEW_VERSION是step2所花费的总时间。您可以将其与您自己的测量结果进行比较。 (2认同)
  • @Nfff3我认为,这在很大程度上取决于类已经使用了多少以及如何使用。换句话说,需要进行多少去优化才能应用更改。 (2认同)