如通过 JNI 访问 Java 对象、为什么 JNI 访问要比直接 访问 对象 要慢?

如通过 JNI 访问 Java 对象、为什么 JNI 访问要比直接 访问 对象 要慢?

对象本身有虚函数表,连接好了类型完成后,代码中的调用可能直接连接好,对应的直接 是方法本身,或者虚函数表的索引

而反射最终是转到 JNI,反射前就跌解析、校验、查找一下,方法名对应的 方法实体,拿到虚函数表,调用

而直接调用是通过 virtualInvoke实现的,反射的优化用时间换空间,直接再生成一个类,反射调用转成直接调用

static void jni_invoke_nonstatic(JNIEnv env, JavaValue result, jobject receiver, JNICallType call_type, jmethodID method_id, JNI_ArgumentPusher *args, TRAPS) {

oop recv = JNIHandles::resolve(receiver);

if (recv == NULL) {

THROW(vmSymbols::java_lang_NullPointerException());

}

Handle h_recv(THREAD, recv);

int number_of_parameters;

Method* selected_method;

{

Method* m = Method::resolve_jmethod_id(method_id);

number_of_parameters = m->size_of_parameters();

Klass* holder = m->method_holder();

if (!(holder)->is_interface()) {

// non-interface call -- for that little speed boost, don't handlize

debug_only(No_Safepoint_Verifier nosafepoint;)

if (call_type == JNI_VIRTUAL) {

// jni_GetMethodID makes sure class is linked and initialized

// so m should have a valid vtable index.

assert(!m->has_itable_index(), "");

int vtbl_index = m->vtable_index();

if (vtbl_index != Method::nonvirtual_vtable_index) {

Klass* k = h_recv->klass();

// k might be an arrayKlassOop but all vtables start at

// the same place. The cast is to avoid virtual call and assertion.

InstanceKlass ik = (InstanceKlass)k;

selected_method = ik->method_at_vtable(vtbl_index);

} else {

// final method

selected_method = m;

}

} else {

// JNI_NONVIRTUAL call

selected_method = m;

}

} else {

// interface call

KlassHandle h_holder(THREAD, holder);

int itbl_index = m->itable_index();

Klass* k = h_recv->klass();

selected_method = InstanceKlass::cast(k)->method_at_itable(h_holder(), itbl_index, CHECK);

}

}

JNI提供了很多函数来操作对象。这意味着,本地方法的实现不需要关心虚拟机内部如何表示对象。这项关键的设计决定是JNI不必 关心VM的内部实现。

使用JNI函数来通过引用间接操作对象比使用指针直接操作C中的对象要慢。但是,我们认为这很值得。

JNI提供了很多函数来操作对象。这意味着,本地方法的实现不需要关心虚拟机内部如何表示对象。这项关键的设计决定是JNI不必 关心VM的内部实现。

使用JNI函数来通过引用间接操作对象比使用指针直接操作C中的对象要慢。但是,我们认为这很值得。

访问基本类型数组

访问数组时,如果用JNI函数重复调用访问其中的每一个元素,那么消耗是相当大的。 一个解决方案是引入一种“pinning”机制,这样JVM就不会再移走数组内容。本地方法接受一个指向这些元素的直接指针。但这有两个 影响:

JVM的GC必须支持“pinning”。“pinning”机制在JVM中并不一定被实现,因为它会使GC的算法更复杂,并有可能导致内存碎片。

JVM必须在内存中连续地存放数组。虽然这是大部分基本类型数组的默认实现方式,但是boolean数组是比较特殊的一个。Boolean数 组有两种方式,packed和unpacked。用packed实现方式时,每个元素用一个bit来存放一个元素,而unpacked使用一个字节来存放一个 元素。因此,依赖于boolean数组特定存放方式的本地代码将是不可移植的。

JNI采用了一个折衷方案来解决上面这两个问题:

JNI提供了一系列函数(例如,GetIntArrayRegion、SetIntArrayRegion)把基本类型数组复制到本地的内存缓存。如果本地代码 需要访问数组当中的少量元素,或者必须要复制一份的话,请使用这些函数。

程序可以使用另外一组函数(例如,GetIntArrayElement)来获取数组被pinning后的直接指针。如果VM不支持pinning,这组函数会 返回数组的复本。

当数组使用完后,本地代码会调用另外一组函数(例如,ReleaseInt-ArrayElement)来通知JVM。这时,JVM会unpin数组或者把对复 制后的数组的改变反映到原数组上然后释放复制后的数组。

这组函数(GetIntArrayElement)是否会复制数组,取决于下面两点:

如果GC支持pinning,并且数组的布局和本地相同类型的数组布局一样,就不会发生复制。

否则的话,数组被复制到一个不可变的内存块儿中(例如,C的heap上面)并做一些格式转换。并把复制品的指针返回。

这种方式提供了很大的灵活性。GC算法可以自由决定是复制数组,或者pin数组,还是复制小数组,pinning大数组。 JNI函数必须确保不同线程的本地方法可以同步访问相同的数组。例如,JNI可能会为每一个被pinning的数组保持一个计数器,如果数 组被两个线程pin的话,其中一个unpin不会影响另一个线程。

字段和方法 JNI允许本地代码通过名字和类型描述符来访问JAVA中的字段或调用JAVA中的方法。 例如,为了读取类cls中的一个int实例字段:

//本地方法首先要获取字段ID

jfieldID fid = env->GetFieldID(env, cls, "i", "I");

//然后可以多次使用这个ID,不需要再次查找,除非JVM把定义这个字段和方法的类或者接口unload,字段ID和方法ID会一直有效。

jint value = env->GetIntField(env, obj, fid);

字段和方法可以来自定个类或接口,也可以来自它们的父类或间接父类。JVM规范规定:如果两个类或者接口定义了相同的字段和方法 ,那么它们返回的字段ID和方法ID也一定会相同。例如,如果类B定义了字段fld,类C从B继承了字段fld,那么程序从这两个类上获 取到的名字为“fld”的字段的字段ID是相同的。

JNI不会规定字段ID和方法ID在JVM内部如何实现。

通过JNI,程序只能访问那些已经知道名字和类型的字段和方法。而使用Java CoreReflection机制提供的API,程序员不用知道具体的 信息就可以访问字段或者调用方法。有时在本地代码中调用反射机制也很有用。所以,JDK提供了一组API来在JNI字段ID和 java.lang.reflect.Field 类的实例之间转换,另外一组在JNI方法ID和java.lang.reflect.Method类实例之间转换。

http://luori366.github.io/JNI_doc/jni_design_theory.html

相关风暴

太极熊猫3阵营哪个好 阵营选择技巧攻略
英国手机版365

太极熊猫3阵营哪个好 阵营选择技巧攻略

🌀 08-20 🌊 阅读 3777
Magisk模块合集2025/7/16 10:36:00导言
28365365体育官网

Magisk模块合集2025/7/16 10:36:00导言

🌀 08-07 🌊 阅读 1787
护发素的6种神级英文表达
英国手机版365

护发素的6种神级英文表达

🌀 06-27 🌊 阅读 956