如何加速运行时 Java 代码检测?
How to speed up runtime Java code instrumentation?
我制作了一个 Java 代理程序,它在 运行时 期间附加到 JVM 并检测所有加载的项目 classes 并插入一些日志记录语句。总共有 11k classes。我测量了我的 ClassFileTransformer
的 transform
方法所花费的总时间,它是 3 秒。但整个检测过程的持续时间大约需要 30 秒。
这就是我重新转换 classes:
的方式
instrumentation.retransformClasses(myClassesArray);
我假设 JVM 占用了大部分时间来重新加载更改的 classes。那正确吗?我怎样才能加快检测过程?
更新:
当我的代理被附加时,
instrumentation.addTransformer(new MyTransfomer(), true);
instrumentation.retransformClasses(retransformClassArray);
只调用了一次。
然后 MyTransfomer
class 检测 classes 并测量检测的总持续时间:
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;
}
}
在检测完所有 classes 之后(从初始数组)(全局缓存跟踪检测过的 classes)total
被打印出来,它将是 ~ 3秒。但是 max-min
大约是 30 秒。
更新二:
查看堆栈跟踪后,会发生以下情况:
我叫
instrumentation.retransformClasses(retransformClassArray);
调用本机方法retransformClasses0()
。一段时间后(!)JVM 调用 sun.instrument.InstrumentationImpl
class 的 transform()
方法(但是这个方法一次只需要一个 class,所以 JVM 调用这个方法连续多次),它在 sun.instrument.TransformerManager
对象上调用 transform()
,该对象有一个包含所有 ClassTransformers
注册的列表,并调用这些转换器中的每一个来转换 class(我只注册了一个变压器!!).
所以在我看来,大部分时间都花在了 JVM 上(在调用 retransformClasses0()
之后和每次调用 sun.instrument.InstrumentationImpl.transform()
之前)。有没有办法减少 JVM 执行此任务所需的时间?
根据您的描述,完整的转换似乎是 运行 在单个线程中进行的。
您可以创建多个线程,每个线程同时转换一个 class。由于 class 的转换应该独立于任何其他 class。这应该可以使您在整体转换时间上有所改善,这是执行系统上可用核心数量的一个因素。
您可以通过以下方式计算内核数:
int cores = Runtime.getRuntime().availableProcessors();
将要转换为核心数的 classes 列表分块,并创建可能的线程以并行处理这些块。
更正:
因为 retransformClasses(classArr)
不会立即重新转换 classArr
中的所有元素,而是会根据需要重新转换每个元素(例如,在链接时)。(请参阅 jdk [VM_RedefineClasses
][1 ] 和 [jvmtiEnv
][2]),它会同时 重新转换 所有这些。
retransformClasses() 的作用:
- 将控制转移到原生层,并给它一个class我们要转换的列表
- 对于每个要转换的 class,本机代码会尝试通过调用我们的 java 转换器来获取新版本,这会导致 java 代码之间的控制转移和原生。
- 本机代码用给定的新 class 版本相互替换内部表示的适当部分。
在第 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
实现,本程序会post一个ClassFileLoadHook
事件,其回调可以通过调用javatransformer方法获取转换后的字节码。这一步控件会在原生代码和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)
方法将目标 class 中的部分(例如常量池、方法等)替换为转换后的 class.
中的部分
// 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 的工作方式是不可能的,所以多线程可能不是一个坏主意。在我的演示项目中使用多线程后,速度提高了好几倍。
我制作了一个 Java 代理程序,它在 运行时 期间附加到 JVM 并检测所有加载的项目 classes 并插入一些日志记录语句。总共有 11k classes。我测量了我的 ClassFileTransformer
的 transform
方法所花费的总时间,它是 3 秒。但整个检测过程的持续时间大约需要 30 秒。
这就是我重新转换 classes:
instrumentation.retransformClasses(myClassesArray);
我假设 JVM 占用了大部分时间来重新加载更改的 classes。那正确吗?我怎样才能加快检测过程?
更新:
当我的代理被附加时,
instrumentation.addTransformer(new MyTransfomer(), true);
instrumentation.retransformClasses(retransformClassArray);
只调用了一次。
然后 MyTransfomer
class 检测 classes 并测量检测的总持续时间:
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;
}
}
在检测完所有 classes 之后(从初始数组)(全局缓存跟踪检测过的 classes)total
被打印出来,它将是 ~ 3秒。但是 max-min
大约是 30 秒。
更新二:
查看堆栈跟踪后,会发生以下情况: 我叫
instrumentation.retransformClasses(retransformClassArray);
调用本机方法retransformClasses0()
。一段时间后(!)JVM 调用 sun.instrument.InstrumentationImpl
class 的 transform()
方法(但是这个方法一次只需要一个 class,所以 JVM 调用这个方法连续多次),它在 sun.instrument.TransformerManager
对象上调用 transform()
,该对象有一个包含所有 ClassTransformers
注册的列表,并调用这些转换器中的每一个来转换 class(我只注册了一个变压器!!).
所以在我看来,大部分时间都花在了 JVM 上(在调用 retransformClasses0()
之后和每次调用 sun.instrument.InstrumentationImpl.transform()
之前)。有没有办法减少 JVM 执行此任务所需的时间?
根据您的描述,完整的转换似乎是 运行 在单个线程中进行的。
您可以创建多个线程,每个线程同时转换一个 class。由于 class 的转换应该独立于任何其他 class。这应该可以使您在整体转换时间上有所改善,这是执行系统上可用核心数量的一个因素。
您可以通过以下方式计算内核数:
int cores = Runtime.getRuntime().availableProcessors();
将要转换为核心数的 classes 列表分块,并创建可能的线程以并行处理这些块。
更正:
因为 ,它会同时 重新转换 所有这些。retransformClasses(classArr)
不会立即重新转换 classArr
中的所有元素,而是会根据需要重新转换每个元素(例如,在链接时)。(请参阅 jdk [VM_RedefineClasses
][1 ] 和 [jvmtiEnv
][2])
retransformClasses() 的作用:
- 将控制转移到原生层,并给它一个class我们要转换的列表
- 对于每个要转换的 class,本机代码会尝试通过调用我们的 java 转换器来获取新版本,这会导致 java 代码之间的控制转移和原生。
- 本机代码用给定的新 class 版本相互替换内部表示的适当部分。
在第 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
实现,本程序会post一个ClassFileLoadHook
事件,其回调可以通过调用javatransformer方法获取转换后的字节码。这一步控件会在原生代码和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)
方法将目标 class 中的部分(例如常量池、方法等)替换为转换后的 class.
// 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 的工作方式是不可能的,所以多线程可能不是一个坏主意。在我的演示项目中使用多线程后,速度提高了好几倍。