如何加速运行时Java代码仪器化?

6

我制作了一个Java代理程序,它在运行时附加到JVM上,并检测所有已加载的项目类并插入一些日志语句。总共有11k个类。我测量了我的的方法所需的总时间,大约为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需要连续多次调用该方法),该方法又会调用sun.instrument.TransformerManager对象的transform()方法,该对象具有已注册的所有ClassTransformers列表,并依次调用每个转换器来转换类(我只注册了一个转换器!)。

因此,在JVM中花费的大部分时间(在调用retransformClasses0()之后,在每次调用sun.instrument.InstrumentationImpl.transform()之前)是最耗时的。是否有办法减少JVM执行此任务所需的时间?


2
没有源代码、Java版本和类路径几乎不可能提供帮助。您能否将其添加到问题中或创建一个Github项目? - Jeff
1
非常需要查看transformInner在做什么,如果可能的话。此外,我建议记录每个类的执行时间,以查看是否存在特定类的问题。 - Jeff
1
JVM 中是否还注册了其他的类文件转换器? - Holger
实际转换的代码丢失,因此无法分析瓶颈。 - Simulant
@Holger,请查看我的第二次更新,我确实只注册了一个变压器。如果我有更多的变压器,那么执行时间更长就有意义了,因为它们是串行执行的。 - Nfff3
显示剩余2条评论
2个回答

2

更正:

由于retransformClasses(classArr)不会一次性重组classArr中的所有元素,而是在需要时(例如链接时)逐个进行重组。 (请参阅JDK [VM_RedefineClasses][1]和[jvmtiEnv ][2]),因此它只会同时对它们进行retransform 实际上,它会一次性对它们进行所有的retransform

retransformClasses()的功能:

  1. 将控制权转移至本地层,并向其提供要转换的类列表
  2. 对于每个要变换的类,本地代码尝试通过调用我们的Java转换器来获取新版本,这导致Java代码和本地代码之间的控制权转移。
  3. 本地代码使用给定的新类版本之一替换内部表示的相应部分。

在步骤1中:

java.lang.instrument.Instrumentation#retransformClasses调用sun.instrument.InstrumentationImpl#retransformClasses0,它是一个JNI方法,控制权将被转移到本地层。

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

第二步:

这一步由KlassFactory::create_from_stream实现,该过程将发布一个ClassFileLoadHook事件,其回调可以通过调用Java变换器方法来获取转换后的字节码。在此步骤中,控制权将在本机代码和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);

在第三步中:VM_RedefineClasses::redefine_single_class(jclass the_jclass, InstanceKlass* scratch_class, TRAPS) 方法将目标类的部分(如常量池、方法等)替换为变换类的部分。请注意保留 HTML 标签。
// 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代码仪器?

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

由于无法改变jvm的工作方式,因此多线程可能不是一个坏主意。在我的演示项目中使用多线程后,速度提高了数倍。


我还有一个问题,linking 究竟是在什么时候发生的?据我所知,当我加载我的代理时,所有我想要检测(classArr)的类都会立即被检测,即使它们上面没有调用任何方法(基本上当代理被加载时,我的应用程序处于暂停状态)。 - Nfff3
1
@Nfff3是这样吗?此答案描述了转换过程中执行的一些步骤,并建议更改您的代理,而不是提高速度,只是更改您正在测量的内容。它如何解决您的问题?我进行了一些实验,在我的设置中,11k个类在不到一秒钟内被转换。那么为什么在您的情况下速度如此缓慢,您是如何解决的?如果此答案说了,那么它肯定非常隐蔽。 - Holger
2
我正在开发一个本地代理,转换过程有点复杂。如果需要的话,我可以提供一个简单的POC项目来帮助你。你可以参考https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-5.html获取更多关于“加载、链接和初始化”的信息。稍后我会补充这个答案。 - Rieon Ke
2
你的转换过程为何如此缓慢,不妨尝试我这个JDK分支https://github.com/rieonke/jdk/tree/agent_measure,它会在转换时打印一些日志,其中M_CALL_INSTRU_IMPL表示sun.instrument.InstrumentationImpl#transform所花费的时间,而M_LOAD_NEW_VERSION则是步骤2所花费的总时间。你可以将其与自己的测量结果进行对比。 - Rieon Ke
2
@Nfff3 我认为这在很大程度上取决于类已经被使用了多少,以及如何使用。换句话说,需要应用更改的去优化程度有多大。 - Holger
显示剩余4条评论

0

根据您的描述,似乎完整的转换是在单个线程中运行。

您可以创建多个线程,每个线程一次转换一个类。由于一个类的转换应该独立于任何其他类,因此这应该将执行系统上可用的核心数量的总体转换时间提高了一个因子。

您可以使用以下命令计算内核数:

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

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

1
我不认为这是问题所在。请看我的第二次更新。 - Nfff3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接