本帖最后由 renyunzhong 于 2021-3-1 21:33 编辑
这是我发的第一个教程,如有瑕疵请指正~
其实这个教程核心跟插件不搭边,不过既然是写给NMS用的,那也算是插件教程吧。
虽然这里是在研究使用JNI实现反射,其实也是在讨论本地库与Bukkit结合使用的一种可能。
这篇文章旨在通过JNI提高反射速度,但这样可能并不能大幅提高依赖反射的插件的性能,因为相比调用MC和BukkitAPI,使用反射的性能开销相当小,所以如果想提高自己插件的性能,应该先考虑优化算法或使用多线程,其次再考虑使用JNI这种“歪门邪道”。
文中的测试均为for循环64次,总共十次取平均值,为什么只有64次呢,因为次数多了会触发JVM一些奇奇怪怪优化。
文中使用Java版本为JDK11,C编译器为MinGW64,CPU为i9-9900k,使用Windows 10
能点进来的大概都知道反射能用来做什么。。。
。。。我还是简单介绍下吧
什么是反射呢?
反射是一种执行你先前无法知道运行时方法或域的功能。
比较绕?看看实例:
BukkitMC核心类文件路径为net.minecraft.server.vXX_XX.Clazz
中间的包名vXX_XX即是MC版本,也就是说每个MC版本这块都会不一样
很明显正常方式调用的话版本就得写死中间版本,但这样就会让插件丧失跨版本能力
即使使用穷举法import所有老版本的类,将来新版本MC出现插件不升级仍然用不了(你不能指望编译器从未来获得不存在的包的相关信息)
而此时使用反射,就能通过类完整路径直接获取类对象,进而获取其中方法进行执行
比如:
等价于
反射虽然很好用,但缺点也很明显,通过字符串寻找类和方法非常耗时,同时Method.invoke()方法由于层层的安全、权限检查和其他杂项导致其耗时是直接调用的十几倍甚至几十倍,对于类Example测试的结果是:
反射:Time used: 0.23186ms
那么有什么办法提高反射性能吗?
<del>比如java.lang.invoke.MethodHandle(这个东西比反射还慢)</del>
比如使用JNI和动态代码(比如ReflectASM库)生成,前者缩小差距到几倍以内,而后者速度和直接调用几乎是相同的。
这里我们使用前者来达成目标
首先使用JNI需要你同时掌握Java和C语言,下面涉及C语言的部分我会尽量写的简单易懂。
JNI(Java Native Interface),Java本地接口,是给本地代码(C和C++)用来访问JVM内容的接口,其所有的函数都以函数指针(方法)形式存在于JNIEnv这个结构体(类似于一个类)中。
其中有两个函数叫CallObjectMethod(调用返回Object的成员方法)和CallStaticObjectMethod(调用返回Object的静态方法)。
于是对于上方的foo(),我们就可以定义一个本地方法:
创建一个JNIReflect.c文件并包含JNIReflect.h并实现其中的方法:
第一行通过FromReflectedMethod(JNIEnv * env, jobject method)函数获取到了传进来的Method对象所代表方法在JVM内部的ID
第二行主要获取Method.clazz这个域对应的内部ID,里面的GetObjectClass(JNIEnv * env, jobject obj)函数与Object.getClass()对应,后面的神秘代码"Ljava/lang/Class;"是域的特征码,具体可以写一个包含Class<?> clazz;域的空壳类编译后使用javap -v <类名> 进行查看。
第三行GetObjectField(JNIEnv * env, jobject obj, jfieldID id)函数与Field.get()对应,获取Method.clazz域上的那个类,就是方法所属的类
最后一行通过CallStaticObjectMethod(JNIEnv * env, jclass method_class, jmethodID id, ...)函数执行即可;
(函数开头的JNIEXPORT和JNICALL分别是导出dll函数的,和Win32标准函数的声明,不用深究)
编译为dll后放在项目Source目录下,使用System.load(new File("dll名字含后缀").getAbsolutePath())加载dll后即可调用本地方法了。
测试结果:
直接:Time used: 0.00278ms
反射:Time used: 0.30192ms
JNI :Time used: 0.00770ms
可以看到相比传统反射,使用JNI效率相当可观。
那怎么实现Method.invoke()那种支持变长参数的呢?
首先要知道Java中的变长参数本质是数组,然后就好处理了:
将jobjectArray(Object[])转为C语言的jvalue(所有Java内变量类型的超集,包括基础类型)指针数组,传入下方调用方法的函数中。
当然现在这个函数对于包含基础类型参数和返回值的函数支持很差,因为这涉及到Java的拆装箱等问题,完整版请看文章最后的源码。
其中寻找方法ID,域ID都可以使用全局引用缓存起来,调用的方法找到的方法ID也可以使用C语言版的Queue或者C++的queue缓存。
说了这么多,终于把JNI执行方法这部分讲完了,不过查找域的值和调用构造器都是同理了,相比原版反射都能提高不少,虽然读写域值这块使用sun.misc.Unsafe或jdk.internal.misc.Unsafe可以更快,但这个东西被Java藏起来了所以。。。
最后一部分说一下插件加载dll的注意事项(终于回到MC了):
1. 编写插件时,dll需要包含在Jar内但不能作为资源打包,因为这样会损坏掉,所以需要作为额外文件打包进Jar:在IDEA中,在Artifacts下Output Layout下方右击jar选择Add copy of中File,找到dll文件添加即可。
2. dll功能需要经过仔细严格的测试再实装进mc服务器,因为C代码bug导致的问题不是抛个异常能解决的,大概率直接导致JVM崩溃。
3. 直接使用System.load()加载dll会导致服务器重启后使用dll的插件产生UnsatisfiedLinkError。
对于第三条的解决方法:
1. 禁止服务器重载
2. 使用自定义类加载器隔离使用dll的类和其他类,中间使用接口链接,这样类加载器被删除引用后System.gc()调用GC就会把类和dll一起回收掉,从而在重载时能重新加载dll,具体实现请看源码中接口部分。
后记:理论上这个代码全平台通用,但C语言编译出来的东西不跨平台,所以编译出dll基本就是Windows专属。
其实这个教程核心跟插件不搭边,不过既然是写给NMS用的,那也算是插件教程吧。
虽然这里是在研究使用JNI实现反射,其实也是在讨论本地库与Bukkit结合使用的一种可能。
这篇文章旨在通过JNI提高反射速度,但这样可能并不能大幅提高依赖反射的插件的性能,因为相比调用MC和BukkitAPI,使用反射的性能开销相当小,所以如果想提高自己插件的性能,应该先考虑优化算法或使用多线程,其次再考虑使用JNI这种“歪门邪道”。
文中的测试均为for循环64次,总共十次取平均值,为什么只有64次呢,因为次数多了会触发JVM一些奇奇怪怪优化。
文中使用Java版本为JDK11,C编译器为MinGW64,CPU为i9-9900k,使用Windows 10
能点进来的大概都知道反射能用来做什么。。。
。。。我还是简单介绍下吧
什么是反射呢?
反射是一种执行你先前无法知道运行时方法或域的功能。
比较绕?看看实例:
BukkitMC核心类文件路径为net.minecraft.server.vXX_XX.Clazz
中间的包名vXX_XX即是MC版本,也就是说每个MC版本这块都会不一样
很明显正常方式调用的话版本就得写死中间版本,但这样就会让插件丧失跨版本能力
即使使用穷举法import所有老版本的类,将来新版本MC出现插件不升级仍然用不了(你不能指望编译器从未来获得不存在的包的相关信息)
而此时使用反射,就能通过类完整路径直接获取类对象,进而获取其中方法进行执行
比如:
反射虽然很好用,但缺点也很明显,通过字符串寻找类和方法非常耗时,同时Method.invoke()方法由于层层的安全、权限检查和其他杂项导致其耗时是直接调用的十几倍甚至几十倍,对于类Example测试的结果是:
直接:Time used: 0.00269ms
反射:Time used: 0.23186ms
那么有什么办法提高反射性能吗?
<del>比如java.lang.invoke.MethodHandle(这个东西比反射还慢)</del>
比如使用JNI和动态代码(比如ReflectASM库)生成,前者缩小差距到几倍以内,而后者速度和直接调用几乎是相同的。
这里我们使用前者来达成目标
首先使用JNI需要你同时掌握Java和C语言,下面涉及C语言的部分我会尽量写的简单易懂。
JNI(Java Native Interface),Java本地接口,是给本地代码(C和C++)用来访问JVM内容的接口,其所有的函数都以函数指针(方法)形式存在于JNIEnv这个结构体(类似于一个类)中。
其中有两个函数叫CallObjectMethod(调用返回Object的成员方法)和CallStaticObjectMethod(调用返回Object的静态方法)。
于是对于上方的foo(),我们就可以定义一个本地方法:
通过在类文件目录下执行javac -h JNIReflect JNIReflect.java生成一个包含JNIReflect.h文件的同名文件夹
创建一个JNIReflect.c文件并包含JNIReflect.h并实现其中的方法:
在JNI中,所有Java对象均为jobject,也就是全都是Object,这里与Java反射非常像。
第一行通过FromReflectedMethod(JNIEnv * env, jobject method)函数获取到了传进来的Method对象所代表方法在JVM内部的ID
第二行主要获取Method.clazz这个域对应的内部ID,里面的GetObjectClass(JNIEnv * env, jobject obj)函数与Object.getClass()对应,后面的神秘代码"Ljava/lang/Class;"是域的特征码,具体可以写一个包含Class<?> clazz;域的空壳类编译后使用javap -v <类名> 进行查看。
第三行GetObjectField(JNIEnv * env, jobject obj, jfieldID id)函数与Field.get()对应,获取Method.clazz域上的那个类,就是方法所属的类
最后一行通过CallStaticObjectMethod(JNIEnv * env, jclass method_class, jmethodID id, ...)函数执行即可;
(函数开头的JNIEXPORT和JNICALL分别是导出dll函数的,和Win32标准函数的声明,不用深究)
编译为dll后放在项目Source目录下,使用System.load(new File("dll名字含后缀").getAbsolutePath())加载dll后即可调用本地方法了。
测试结果:
直接:Time used: 0.00278ms
反射:Time used: 0.30192ms
JNI:Time used: 0.00770ms
可以看到相比传统反射,使用JNI效率相当可观。
那怎么实现Method.invoke()那种支持变长参数的呢?
首先要知道Java中的变长参数本质是数组,然后就好处理了:
将jobjectArray(Object[])转为C语言的jvalue(所有Java内变量类型的超集,包括基础类型)指针数组,传入下方调用方法的函数中。
当然现在这个函数对于包含基础类型参数和返回值的函数支持很差,因为这涉及到Java的拆装箱等问题,完整版请看文章最后的源码。
其中寻找方法ID,域ID都可以使用全局引用缓存起来,调用的方法找到的方法ID也可以使用C语言版的Queue或者C++的queue缓存。
说了这么多,终于把JNI执行方法这部分讲完了,不过查找域的值和调用构造器都是同理了,相比原版反射都能提高不少,虽然读写域值这块使用sun.misc.Unsafe或jdk.internal.misc.Unsafe可以更快,但这个东西被Java藏起来了所以。。。
最后一部分说一下插件加载dll的注意事项(终于回到MC了):
1. 编写插件时,dll需要包含在Jar内但不能作为资源打包,因为这样会损坏掉,所以需要作为额外文件打包进Jar:在IDEA中,在Artifacts下Output Layout下方右击jar选择Add copy of中File,找到dll文件添加即可。
2. dll功能需要经过仔细严格的测试再实装进mc服务器,因为C代码bug导致的问题不是抛个异常能解决的,大概率直接导致JVM崩溃。
3. 直接使用System.load()加载dll会导致服务器重启后使用dll的插件产生UnsatisfiedLinkError。
对于第三条的解决方法:
1. 禁止服务器重载
2. 使用自定义类加载器隔离使用dll的类和其他类,中间使用接口链接,这样类加载器被删除引用后System.gc()调用GC就会把类和dll一起回收掉,从而在重载时能重新加载dll,具体实现请看源码中接口部分。
后记:理论上这个代码全平台通用,但C语言编译出来的东西不跨平台,所以编译出dll基本就是Windows专属。
源码奉上
https://github.com/HenryRenYz/CReflection
更新日志:2020.3.1修复代码格式问题
这是我发的第一个教程,如有瑕疵请指正~
其实这个教程核心跟插件不搭边,不过既然是写给NMS用的,那也算是插件教程吧。
虽然这里是在研究使用JNI实现反射,其实也是在讨论本地库与Bukkit结合使用的一种可能。
这篇文章旨在通过JNI提高反射速度,但这样可能并不能大幅提高依赖反射的插件的性能,因为相比调用MC和BukkitAPI,使用反射的性能开销相当小,所以如果想提高自己插件的性能,应该先考虑优化算法或使用多线程,其次再考虑使用JNI这种“歪门邪道”。
文中的测试均为for循环64次,总共十次取平均值,为什么只有64次呢,因为次数多了会触发JVM一些奇奇怪怪优化。
文中使用Java版本为JDK11,C编译器为MinGW64,CPU为i9-9900k,使用Windows 10
能点进来的大概都知道反射能用来做什么。。。
。。。我还是简单介绍下吧
什么是反射呢?
反射是一种执行你先前无法知道运行时方法或域的功能。
比较绕?看看实例:
BukkitMC核心类文件路径为net.minecraft.server.vXX_XX.Clazz
中间的包名vXX_XX即是MC版本,也就是说每个MC版本这块都会不一样
很明显正常方式调用的话版本就得写死中间版本,但这样就会让插件丧失跨版本能力
即使使用穷举法import所有老版本的类,将来新版本MC出现插件不升级仍然用不了(你不能指望编译器从未来获得不存在的包的相关信息)
而此时使用反射,就能通过类完整路径直接获取类对象,进而获取其中方法进行执行
比如:
String version = "v1_16_R3"; // 可以是任意版本Class<?> cls = Class.forName("net.minecraft.server." + version + ".Clazz");
Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
net.minecraft.server.v1_16_R3.Clazz.init("Bar!", 5);
反射虽然很好用,但缺点也很明显,通过字符串寻找类和方法非常耗时,同时Method.invoke()方法由于层层的安全、权限检查和其他杂项导致其耗时是直接调用的十几倍甚至几十倍,对于类Example测试的结果是:
直接:Time used: 0.00269msclass Example {
static void foo(String str, int i) {
String ss = str + i;
String version = "v1_16_R3"; // 可以是任意版本
Class<?> cls = Class.forName("net.minecraft.server." + version + ".Clazz");
Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
}
}
反射:Time used: 0.23186ms
那么有什么办法提高反射性能吗?
<del>比如java.lang.invoke.MethodHandle(这个东西比反射还慢)</del>
比如使用JNI和动态代码(比如ReflectASM库)生成,前者缩小差距到几倍以内,而后者速度和直接调用几乎是相同的。
这里我们使用前者来达成目标
首先使用JNI需要你同时掌握Java和C语言,下面涉及C语言的部分我会尽量写的简单易懂。
JNI(Java Native Interface),Java本地接口,是给本地代码(C和C++)用来访问JVM内容的接口,其所有的函数都以函数指针(方法)形式存在于JNIEnv这个结构体(类似于一个类)中。
其中有两个函数叫CallObjectMethod(调用返回Object的成员方法)和CallStaticObjectMethod(调用返回Object的静态方法)。
于是对于上方的foo(),我们就可以定义一个本地方法:
通过在类文件目录下执行javac -h JNIReflect JNIReflect.java生成一个包含JNIReflect.h文件的同名文件夹public class JNIReflect {
public static native void invoke(Method mth, String str, int t);
}
创建一个JNIReflect.c文件并包含JNIReflect.h并实现其中的方法:
在JNI中,所有Java对象均为jobject,也就是全都是Object,这里与Java反射非常像。JNIEXPORT void JNICALL Java_JNIReflect_invokeVaArgs
(JNIEnv * env, jclass cls, jobject mth, jstring str, jint i) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
(*env)->CallStaticObjectMethodA(env, mth_cls, id, str, i);
}
第一行通过FromReflectedMethod(JNIEnv * env, jobject method)函数获取到了传进来的Method对象所代表方法在JVM内部的ID
第二行主要获取Method.clazz这个域对应的内部ID,里面的GetObjectClass(JNIEnv * env, jobject obj)函数与Object.getClass()对应,后面的神秘代码"Ljava/lang/Class;"是域的特征码,具体可以写一个包含Class<?> clazz;域的空壳类编译后使用javap -v <类名> 进行查看。
第三行GetObjectField(JNIEnv * env, jobject obj, jfieldID id)函数与Field.get()对应,获取Method.clazz域上的那个类,就是方法所属的类
最后一行通过CallStaticObjectMethod(JNIEnv * env, jclass method_class, jmethodID id, ...)函数执行即可;
(函数开头的JNIEXPORT和JNICALL分别是导出dll函数的,和Win32标准函数的声明,不用深究)
编译为dll后放在项目Source目录下,使用System.load(new File("dll名字含后缀").getAbsolutePath())加载dll后即可调用本地方法了。
测试结果:
直接:Time used: 0.00278ms
反射:Time used: 0.30192ms
JNI :Time used: 0.00770ms
可以看到相比传统反射,使用JNI效率相当可观。
那怎么实现Method.invoke()那种支持变长参数的呢?
首先要知道Java中的变长参数本质是数组,然后就好处理了:
JNIEXPORT jobject JNICALL Java_JNIReflect_invokeVaArgs
(JNIEnv * env, jclass cls, jobject mth, jobject member, jobjectArray args) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
jfieldID mth_prmtype = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "returnType", "[Ljava/lang/class;");
jobjectArray mth_params_type = (*env)->GetObjectField(env,mth, mth_prmtype);
jint params_count = (*env)->GetArrayLength(env, mth_params_type);
// 传入参数数量和方法需要的不一样,有问题
if (params_count != (*env)->GetArrayLength(env, args)) return NULL;
// 将Java的Object数组转为C的指针数组
jvalue * params = malloc(sizeof(jvalue) * params_count);
for (int i = 0; i < params_count; i++) {
params = (jvalue)(*env)->GetObjectArrayElement(env, args, i);
}
// 判断是否为静态方法,使用member参数非空检查,此方法不可靠,应获取Method.modifiers域的值按位与上Modifier.STATIC的值检查非0
jobject returns = NULL;
if (member != NULL) {
returns = (*env)->CallObjectMethodA(env, member, id, params);
} else {
returns = (*env)->CallStaticObjectMethodA(env, mth_cls, id, params);
}
free(params);
return returns;
}
将jobjectArray(Object[])转为C语言的jvalue(所有Java内变量类型的超集,包括基础类型)指针数组,传入下方调用方法的函数中。
当然现在这个函数对于包含基础类型参数和返回值的函数支持很差,因为这涉及到Java的拆装箱等问题,完整版请看文章最后的源码。
其中寻找方法ID,域ID都可以使用全局引用缓存起来,调用的方法找到的方法ID也可以使用C语言版的Queue或者C++的queue缓存。
说了这么多,终于把JNI执行方法这部分讲完了,不过查找域的值和调用构造器都是同理了,相比原版反射都能提高不少,虽然读写域值这块使用sun.misc.Unsafe或jdk.internal.misc.Unsafe可以更快,但这个东西被Java藏起来了所以。。。
最后一部分说一下插件加载dll的注意事项(终于回到MC了):
1. 编写插件时,dll需要包含在Jar内但不能作为资源打包,因为这样会损坏掉,所以需要作为额外文件打包进Jar:在IDEA中,在Artifacts下Output Layout下方右击jar选择Add copy of中File,找到dll文件添加即可。
2. dll功能需要经过仔细严格的测试再实装进mc服务器,因为C代码bug导致的问题不是抛个异常能解决的,大概率直接导致JVM崩溃。
3. 直接使用System.load()加载dll会导致服务器重启后使用dll的插件产生UnsatisfiedLinkError。
对于第三条的解决方法:
1. 禁止服务器重载
2. 使用自定义类加载器隔离使用dll的类和其他类,中间使用接口链接,这样类加载器被删除引用后System.gc()调用GC就会把类和dll一起回收掉,从而在重载时能重新加载dll,具体实现请看源码中接口部分。
后记:理论上这个代码全平台通用,但C语言编译出来的东西不跨平台,所以编译出dll基本就是Windows专属。
源码奉上
https://github.com/HenryRenYz/CReflection
更新日志:
2020.3.1修复代码格式问题
2021.12 数据,可能有更多内容
这是我发的第一个教程,如有瑕疵请指正~其实这个教程核心跟插件不搭边,不过既然是写给NMS用的,那也算是插件教程吧。
虽然这里是在研究使用JNI实现反射,其实也是在讨论本地库与Bukkit结合使用的一种可能。
这篇文章旨在通过JNI提高反射速度,但这样可能并不能大幅提高依赖反射的插件的性能,因为相比调用MC和BukkitAPI,使用反射的性能开销相当小,所以如果想提高自己插件的性能,应该先考虑优化算法或使用多线程,其次再考虑使用JNI这种“歪门邪道”。
文中的测试均为for循环64次,总共十次取平均值,为什么只有64次呢,因为次数多了会触发JVM一些奇奇怪怪优化。
文中使用Java版本为JDK11,C编译器为MinGW64,CPU为i9-9900k,使用Windows 10
能点进来的大概都知道反射能用来做什么。。。
。。。我还是简单介绍下吧
什么是反射呢?
反射是一种执行你先前无法知道运行时方法或域的功能。
比较绕?看看实例:
BukkitMC核心类文件路径为net.minecraft.server.vXX_XX.Clazz
中间的包名vXX_XX即是MC版本,也就是说每个MC版本这块都会不一样
很明显正常方式调用的话版本就得写死中间版本,但这样就会让插件丧失跨版本能力
即使使用穷举法import所有老版本的类,将来新版本MC出现插件不升级仍然用不了(你不能指望编译器从未来获得不存在的包的相关信息)
而此时使用反射,就能通过类完整路径直接获取类对象,进而获取其中方法进行执行
比如:
String version = "v1_16_R3";// 可以是任意版本Class<?> cls = Class.forName("net.minecraft.server." + version + ".Clazz");
Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
等价于Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
net.minecraft.server.v1_16_R3.Clazz.init("Bar!", 5);
反射虽然很好用,但缺点也很明显,通过字符串寻找类和方法非常耗时,同时Method.invoke()方法由于层层的安全、权限检查和其他杂项导致其耗时是直接调用的十几倍甚至几十倍,对于类Example测试的结果是:
class Example {
static void foo(String str, int i) {
String ss = str + i;
String version = "v1_16_R3";// 可以是任意版本
Class<?> cls = Class.forName("net.minecraft.server." + version + ".Clazz");
Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
}
}
static void foo(String str, int i) {
String ss = str + i;
String version = "v1_16_R3";// 可以是任意版本
Class<?> cls = Class.forName("net.minecraft.server." + version + ".Clazz");
Method mth = cls.getDeclaredMethod("init", String.class, int.class);
mth.invoke(null, "Bar!", 5);
}
}
直接:Time used: 0.00269ms
反射:Time used: 0.23186ms
那么有什么办法提高反射性能吗?
<del>比如java.lang.invoke.MethodHandle(这个东西比反射还慢)</del>
比如使用JNI和动态代码(比如ReflectASM库)生成,前者缩小差距到几倍以内,而后者速度和直接调用几乎是相同的。
这里我们使用前者来达成目标
首先使用JNI需要你同时掌握Java和C语言,下面涉及C语言的部分我会尽量写的简单易懂。
JNI(Java Native Interface),Java本地接口,是给本地代码(C和C++)用来访问JVM内容的接口,其所有的函数都以函数指针(方法)形式存在于JNIEnv这个结构体(类似于一个类)中。
其中有两个函数叫CallObjectMethod(调用返回Object的成员方法)和CallStaticObjectMethod(调用返回Object的静态方法)。
于是对于上方的foo(),我们就可以定义一个本地方法:
public class JNIReflect {
public static native void invoke(Method mth, String str, int t);
}
public static native void invoke(Method mth, String str, int t);
}
通过在类文件目录下执行javac -h JNIReflect JNIReflect.java生成一个包含JNIReflect.h文件的同名文件夹
创建一个JNIReflect.c文件并包含JNIReflect.h并实现其中的方法:
JNIEXPORT void JNICALL Java_JNIReflect_invokeVaArgs
(JNIEnv * env, jclass cls, jobject mth, jstring str, jint i) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
(*env)->CallStaticObjectMethodA(env, mth_cls, id, str, i);
}
(JNIEnv * env, jclass cls, jobject mth, jstring str, jint i) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
(*env)->CallStaticObjectMethodA(env, mth_cls, id, str, i);
}
在JNI中,所有Java对象均为jobject,也就是全都是Object,这里与Java反射非常像。
第一行通过FromReflectedMethod(JNIEnv * env, jobject method)函数获取到了传进来的Method对象所代表方法在JVM内部的ID
第二行主要获取Method.clazz这个域对应的内部ID,里面的GetObjectClass(JNIEnv * env, jobject obj)函数与Object.getClass()对应,后面的神秘代码"Ljava/lang/Class;"是域的特征码,具体可以写一个包含Class<?> clazz;域的空壳类编译后使用javap -v <类名> 进行查看。
第三行GetObjectField(JNIEnv * env, jobject obj, jfieldID id)函数与Field.get()对应,获取Method.clazz域上的那个类,就是方法所属的类
最后一行通过CallStaticObjectMethod(JNIEnv * env, jclass method_class, jmethodID id, ...)函数执行即可;
(函数开头的JNIEXPORT和JNICALL分别是导出dll函数的,和Win32标准函数的声明,不用深究)
编译为dll后放在项目Source目录下,使用System.load(new File("dll名字含后缀").getAbsolutePath())加载dll后即可调用本地方法了。
测试结果:
直接:Time used: 0.00278ms
反射:Time used: 0.30192ms
JNI:Time used: 0.00770ms
可以看到相比传统反射,使用JNI效率相当可观。
那怎么实现Method.invoke()那种支持变长参数的呢?
首先要知道Java中的变长参数本质是数组,然后就好处理了:
JNIEXPORT jobject JNICALL Java_JNIReflect_invokeVaArgs
(JNIEnv * env, jclass cls, jobject mth, jobject member, jobjectArray args) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
jfieldID mth_prmtype = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "returnType", "[Ljava/lang/class;");
jobjectArray mth_params_type = (*env)->GetObjectField(env,mth, mth_prmtype);
jint params_count = (*env)->GetArrayLength(env, mth_params_type);
// 传入参数数量和方法需要的不一样,有问题
if (params_count != (*env)->GetArrayLength(env, args)) return NULL;
// 将Java的Object数组转为C的指针数组
jvalue * params = malloc(sizeof(jvalue) * params_count);
for (int i = 0; i < params_count; i++) {
params = (jvalue)(*env)->GetObjectArrayElement(env, args, i);
}
// 判断是否为静态方法,使用member参数非空检查,此方法不可靠,应获取Method.modifiers域的值按位与上Modifier.STATIC的值检查非0
jobject returns = NULL;
if (member != NULL) {
returns = (*env)->CallObjectMethodA(env, member, id, params);
} else {
returns = (*env)->CallStaticObjectMethodA(env, mth_cls, id, params);
}
free(params);
return returns;
}
(JNIEnv * env, jclass cls, jobject mth, jobject member, jobjectArray args) {
jmethodID id = (*env)->FromReflectedMethod(env, mth);
jfieldID mth_cls_id = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "clazz", "Ljava/lang/Class;");
jclass mth_cls = (*env)->GetObjectField(env, mth, mth_cls_id);
jfieldID mth_prmtype = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, mth), "returnType", "[Ljava/lang/class;");
jobjectArray mth_params_type = (*env)->GetObjectField(env,mth, mth_prmtype);
jint params_count = (*env)->GetArrayLength(env, mth_params_type);
// 传入参数数量和方法需要的不一样,有问题
if (params_count != (*env)->GetArrayLength(env, args)) return NULL;
// 将Java的Object数组转为C的指针数组
jvalue * params = malloc(sizeof(jvalue) * params_count);
for (int i = 0; i < params_count; i++) {
params = (jvalue)(*env)->GetObjectArrayElement(env, args, i);
}
// 判断是否为静态方法,使用member参数非空检查,此方法不可靠,应获取Method.modifiers域的值按位与上Modifier.STATIC的值检查非0
jobject returns = NULL;
if (member != NULL) {
returns = (*env)->CallObjectMethodA(env, member, id, params);
} else {
returns = (*env)->CallStaticObjectMethodA(env, mth_cls, id, params);
}
free(params);
return returns;
}
将jobjectArray(Object[])转为C语言的jvalue(所有Java内变量类型的超集,包括基础类型)指针数组,传入下方调用方法的函数中。
当然现在这个函数对于包含基础类型参数和返回值的函数支持很差,因为这涉及到Java的拆装箱等问题,完整版请看文章最后的源码。
其中寻找方法ID,域ID都可以使用全局引用缓存起来,调用的方法找到的方法ID也可以使用C语言版的Queue或者C++的queue缓存。
说了这么多,终于把JNI执行方法这部分讲完了,不过查找域的值和调用构造器都是同理了,相比原版反射都能提高不少,虽然读写域值这块使用sun.misc.Unsafe或jdk.internal.misc.Unsafe可以更快,但这个东西被Java藏起来了所以。。。
最后一部分说一下插件加载dll的注意事项(终于回到MC了):
1. 编写插件时,dll需要包含在Jar内但不能作为资源打包,因为这样会损坏掉,所以需要作为额外文件打包进Jar:在IDEA中,在Artifacts下Output Layout下方右击jar选择Add copy of中File,找到dll文件添加即可。
2. dll功能需要经过仔细严格的测试再实装进mc服务器,因为C代码bug导致的问题不是抛个异常能解决的,大概率直接导致JVM崩溃。
3. 直接使用System.load()加载dll会导致服务器重启后使用dll的插件产生UnsatisfiedLinkError。
对于第三条的解决方法:
1. 禁止服务器重载
2. 使用自定义类加载器隔离使用dll的类和其他类,中间使用接口链接,这样类加载器被删除引用后System.gc()调用GC就会把类和dll一起回收掉,从而在重载时能重新加载dll,具体实现请看源码中接口部分。
后记:理论上这个代码全平台通用,但C语言编译出来的东西不跨平台,所以编译出dll基本就是Windows专属。
源码奉上
https://github.com/HenryRenYz/CReflection
更新日志:2020.3.1修复代码格式问题
支持
不过"JVM一些奇奇怪怪优化"也包括JIT在内吧...这个不算是干扰项吧
不过"JVM一些奇奇怪怪优化"也包括JIT在内吧...这个不算是干扰项吧
性能测试中跑个几轮几百万遍 warnup 再跑测试内容是正确做法;对某个游戏进行跑分也没有将启动耗时计入平均帧率的计算吧。
反射运行多次后(一般为 15 次)会生成字节码直接调用的实现,并且反射一般情况下会强制内联;MethodHandle 经过 JIT 后可以达到堪比直接调用的性能,不知道从何而来的慢这一说。
反射运行多次后(一般为 15 次)会生成字节码直接调用的实现,并且反射一般情况下会强制内联;MethodHandle 经过 JIT 后可以达到堪比直接调用的性能,不知道从何而来的慢这一说。
本帖最后由 3TUSK 于 2021-3-3 03:04 编辑
补一个 intrinsic method 列表:
OpenJDK 8: https://gist.github.com/apangin/7a9b7062a4bd0cd41fcc
OpenJDK 9: https://gist.github.com/apangin/8bc69f06879a86163e490a61931b37e8
可以发现 HotSpot JVM 的 Intrinsic method 列表中有 Method.invoke 和 MethodHandle 下的一系列方法。
对于 Intrinsic method,在实际调用的时候会直接调用预先准备的优化过的实现,有时候甚至是一条 native instruction。自行实现是不会触发这种优化的。
延伸阅读:https://stackoverflow.com/questi ... -jvm-use-intrinsics
曾任 Google 首席 Java 架构师的 Joshua Bloch 所著的 Effective Java (Second Edition, ISBN 978-0-321-35668-0) 中列出的第 54 个项目即是 "Use native methods judiciously"(谨慎使用 native 方法)。文中列出了 native method 的劣势:
C 是一门跨平台的语言。维基百科上的记载写明了它支持的操作系统是 "Cross-platform"。你只是需要重新编译一次而已。
引用某个操作系统专有的头文件不算。要知道,主流的操作系统或多或少都兼容 POSIX 标准。
匿名者 发表于 2021-3-3 01:31
性能测试中跑个几轮几百万遍 warnup 再跑测试内容是正确做法;对某个游戏进行跑分也没有将启动耗时计入平均 ...
补一个 intrinsic method 列表:
OpenJDK 8: https://gist.github.com/apangin/7a9b7062a4bd0cd41fcc
OpenJDK 9: https://gist.github.com/apangin/8bc69f06879a86163e490a61931b37e8
可以发现 HotSpot JVM 的 Intrinsic method 列表中有 Method.invoke 和 MethodHandle 下的一系列方法。
对于 Intrinsic method,在实际调用的时候会直接调用预先准备的优化过的实现,有时候甚至是一条 native instruction。自行实现是不会触发这种优化的。
延伸阅读:https://stackoverflow.com/questi ... -jvm-use-intrinsics
曾任 Google 首席 Java 架构师的 Joshua Bloch 所著的 Effective Java (Second Edition, ISBN 978-0-321-35668-0) 中列出的第 54 个项目即是 "Use native methods judiciously"(谨慎使用 native 方法)。文中列出了 native method 的劣势:
- 内存数据不再受 JVM 本身的保护,必须自行处理。
- 需要针对每一个平台编译对应的 native 库,抵消 JVM 原本的免编译跨平台优势。
- Native 方法难以调试(至少,你不能直接使用你的 Java IDE 的调试器,得挂 gdb、valgrind 之类的)。楼主也承认了「C代码bug导致的问题不是抛个异常能解决的」。
- 调用 native 方法本身也有开销,有可能效果适得其反。
- 编写 native 方法的过程枯燥,编写出来的东西可读性也不好。就以楼主所写的 JNICALL Java_JNIReflect_invokeVaArgs 为例,如果没有注释,你能很快理解每一行代码的用途吗?
但C语言编译出来的东西不跨平台
C 是一门跨平台的语言。维基百科上的记载写明了它支持的操作系统是 "Cross-platform"。你只是需要重新编译一次而已。
引用某个操作系统专有的头文件不算。要知道,主流的操作系统或多或少都兼容 POSIX 标准。
3TUSK 发表于 2021-3-3 03:00
补一个 intrinsic method 列表:
OpenJDK 8: https://gist.github.com/apangin/7a9b7062a4bd0cd41fcc
感谢指正,JNI的确缺点一点一大堆,不过对于反射随机访问JNI还是有速度优势,只是放到整体来看影响也不是很大