本文介绍 JNI 用户最常遇到的 10 大编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。
JNI 编程缺陷可以分为两类:
· 性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
· 正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。
1.性能缺陷
程序员在使用 JNI 时的 5 大性能缺陷如下:
· 不缓存方法 ID、字段 ID 和类
· 触发数组副本
· 回访(Reaching back)而不是传递参数
· 错误认定本机代码与 Java 代码之间的界限
使用大量本地引用,而未通知 JVM
1.1.不缓存方法 ID、字段 ID 和类
要访问 Java 对象的字段并调用它们的方法,本机代码必须调用FindClass()、GetFieldID()、GetMethodId()和GetStaticMethodID()。对于GetFieldID()、GetMethodID()和GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。
举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:
清单 1. 使用 JNI 调用静态方法
int val=1;
jmethodID method;
jclass cls;
cls = (*env)->FindClass(env,"com/ibm/example/TestClass");
if ((*env)->ExceptionCheck(env)) {
return ERR_FIND_CLASS_FAILED;
}
method = (*env)->GetStaticMethodID(env,cls, "setInfo", "(I)V");
if ((*env)->ExceptionCheck(env)) {
returnERR_GET_STATIC_METHOD_FAILED;
}
(*env)->CallStaticVoidMethod(env, cls,method,val);
if ((*env)->ExceptionCheck(env)) {
returnERR_CALL_STATIC_METHOD_FAILED;
}
当我们每次希望调用方法时查找类和方法 ID 都会产生六个本机调用,而不是第一次缓存类和方法 ID 时需要的两个调用。
缓存会对您应用程序的运行时造成显著的影响。考虑下面两个版本的方法,它们的作用是相同的。清单 2 使用了缓存的字段 ID:
清单 2. 使用缓存的字段 ID
int sumValues2(JNIEnv* env, jobject obj,jobject allValues){
jint avalue = (*env)->GetIntField(env,allValues, a);
jint bvalue =(*env)->GetIntField(env, allValues, b);
jint cvalue =(*env)->GetIntField(env, allValues, c);
jint dvalue =(*env)->GetIntField(env, allValues, d);
jint evalue =(*env)->GetIntField(env, allValues, e);
jint fvalue =(*env)->GetIntField(env, allValues, f);
return avalue + bvalue +cvalue + dvalue + evalue + fvalue;
}
清单 3 没有使用缓存的字段 ID:
int sumValues2(JNIEnv* env, jobject obj,jobject allValues){
jclass cls =(*env)->GetObjectClass(env,allValues);
jfieldID a =(*env)->GetFieldID(env, cls, "a", "I");
jfieldID b =(*env)->GetFieldID(env, cls, "b", "I");
jfieldID c =(*env)->GetFieldID(env, cls, "c", "I");
jfieldID d =(*env)->GetFieldID(env, cls, "d", "I");
jfieldID e =(*env)->GetFieldID(env, cls, "e", "I");
jfieldID f =(*env)->GetFieldID(env, cls, "f", "I");
jint avalue =(*env)->GetIntField(env, allValues, a);
jint bvalue =(*env)->GetIntField(env, allValues, b);
jint cvalue =(*env)->GetIntField(env, allValues, c);
jint dvalue = (*env)->GetIntField(env,allValues, d);jint evalue =(*env)->GetIntField(env, allValues, e);
jint fvalue =(*env)->GetIntField(env, allValues, f);
return avalue + bvalue +cvalue + dvalue + evalue + fvalue
}
清单 2 用 3,572 ms 运行了 10,000,000 次。清单 3 用了 86,217 ms — 多花了 24 倍的时间。
1.2.触发数组副本
JNI 在 Java 代码和本机代码之间提供了一个干净的接口。为了维持这种分离,数组将作为不透明的句柄传递,并且本机代码必须回调 JVM 以便使用 set 和 get 调用操作数组元素。Java 规范让 JVM 实现决定让这些调用提供对数组的直接访问,还是返回一个数组副本。举例来说,当数组经过优化而不需要连续存储时,JVM 可以返回一个副本。(参见参考资料 获取关于 JVM 的信息)。
随后,这些调用可以复制被操作的元素。举例来说,如果您对含有 1,000 个元素的数组调用GetLongArrayElements(),则会造成至少分配或复制 8,000 字节的数据(每个long1,000 元素 * 8 字节)。当您随后使用ReleaseLongArrayElements()更新数组的内容时,需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的GetPrimitiveArrayCritical(),规范仍然准许 JVM 创建完整数组的副本。
GetTypeArrayRegion()和SetTypeArrayRegion()方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。
举例来说,考虑相同方法的两个版本,如清单 4 所示
jlong getElement(JNIEnv* env, jobject obj,jlongArray arr_j,
intelement){
jboolean isCopy;
jlong result;
jlong* buffer_j =(*env)->GetLongArrayElements(env, arr_j, &isCopy);
result = buffer_j[element];
(*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, 0);
return result;
}
jlong getElement2(JNIEnv* env, jobject obj,jlongArray arr_j,
intelement){
jlong result;
(*env)->GetLongArrayRegion(env, arr_j, element,1, &result);
return result;
}
第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时,运行第一个方法 10,000,000 次用了 12,055 ms;而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间!
另一方面,如果您最终要获取数组中的所有元素,则使用GetTypeArrayRegion()逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 4 中这两个getElement()方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。
1.3.回访而不是传递参数
在调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变化不需要改变方法签名。但是,对于 JNI 来说,本机代码必须通过一个或多个 JNI 调用返回到 JVM 以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本机代码过渡到 Java 代码要比普通方法调用开销更大。因此,对于 JNI 来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。
考虑清单 5 中的两个方法,第二个方法假定我们缓存了字段 ID:
清单 5. 两个方法版本
int sumValues(JNIEnv* env, jobject obj,jint a, jint b,jint c, jint d, jint e, jint f){
return a + b + c + d + e + f;
}
int sumValues2(JNIEnv* env, jobject obj,jobject allValues){
jint avalue =(*env)->GetIntField(env, allValues, a);
jint bvalue =(*env)->GetIntField(env, allValues, b);
jint cvalue =(*env)->GetIntField(env, allValues, c);
jint dvalue =(*env)->GetIntField(env, allValues, d);
jint evalue =(*env)->GetIntField(env, allValues, e);
jint fvalue =(*env)->GetIntField(env, allValues, f);
return avalue + bvalue +cvalue + dvalue + evalue + fvalue;
}
sumValues2()方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比sumValues()慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues()避免了大量的 JNI 开销。
1.4.错误认定本机代码与 Java 代码之间的界限
本机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。
因此,在设计 Java 代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。
举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:
清单 6. 到串行端口的接口:版本 1
/**
*Initializes the serial port and returns a java SerialPortConfig objects
*that contains the hardware address for the serial port, and holds
*information needed by the serial port such as the next buffer
* to write data into
*
* @param env JNI env that can be usedby the method
*@param comPortName the name of the serial port
*@returns SerialPortConfig object to be passed ot setSerialPortBit
* and getSerialPortBit calls
*/
jobject initializeSerialPort(JNIEnv* env,jobject obj, jstring comPortName);
/**
*Sets a single bit in an 8 bit byte to be sent by the serial port
*
*@param env JNI env that can be used by the method
*@param serialPortConfig object returned by initializeSerialPort
*@param whichBit value from 1-8 indicating which bit to set
*@param bitValue 0th bit contains bit value to be set
*/
void setSerialPortBit(JNIEnv* env, jobjectobj, jobject serialPortConfig,
jint whichBit, jintbitValue);
/**
*Gets a single bit in an 8 bit byte read from the serial port
*
*@param env JNI env that can be used by the method
*@param serialPortConfig object returned by initializeSerialPort
*@param whichBit value from 1-8 indicating which bit to read
*@returns the bit read in the 0th bit of the jint
*/
jint getSerialPortBit(JNIEnv* env, jobjectobj, jobject serialPortConfig,
jint whichBit);
/**
*Read the next byte from the serial port
*
* @param env JNI env that can be usedby the method
*/
void readNextByte(JNIEnv* env, jobjectobj);
/**
*Send the next byte
*
*@param env JNI env that can be used by the method
*/
void sendNextByte(JNIEnv* env, jobject obj);
在 清单 6 中,串行端口的所有配置数据都存储在由initializeSerialPort()方法返回的 Java 对象中,并且将 Java 代码完全控制对硬件中各数据位的设置。清单 6 所示版本的一些问题会造成其性能差于清单 7 中的版本:
清单 7. 到串行端口的接口:版本 2
/**
*Initializes the serial port and returns an opaque handle to a native
*structure that contains the hardware address for the serial port
* and holds information needed by theserial port such as
* the next buffer to write data into
*
*@param env JNI env that can be used by the method
*@param comPortName the name of the serial port
* @returns opaque handle to bepassed to setSerialPortByte and
/**
*Initializes the serial port and returns an opaque handle to a native
*structure that contains the hardware address for the serial port
* and holds information needed by theserial port such as
* the next buffer to write data into
*
*@param env JNI env that can be used by the method
*@param comPortName the name of the serial port
* @returns opaque handle to bepassed to setSerialPortByte and
* getSerialPortByte calls
*/
jlong initializeSerialPort2(JNIEnv* env,jobject obj, jstring comPortName);
/**
*sends a byte on the serial port
*
* @param env JNI env that can be usedby the method
*@param serialPortConfig opaque handle for the serial port
*@param byte the byte to be sent
*/
void sendSerialPortByte(JNIEnv* env,jobject obj, jlong serialPortConfig,
jbyte byte);
/**
*Reads the next byte from the serial port
*
* @param env JNI env that can be usedby the method
*@param serialPortConfig opaque handle for the serial port
*@returns the byte read from the serial port
*/
jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlongserialPortConfig);
最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。
1.5.使用大量本地引用而未通知JVM
JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用GetObjectArrayElement()时,将返回对数组中对象的本地引用。考虑清单 8 中的代码在运行一个很大的数组时会使用多少本地引用:
清单 8. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj,jarray array){
jint i;
jint count =(*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobjectelement = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)){
break;
}
/* dosomething with array element */
}
}
每次调用GetObjectArrayElement()时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。
这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本机代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了,但一些方法在其生存期中却需要更多的本地引用。对于这种情况,您应该删除不再需要的引用,方法是使用 JNI DeleteLocalRef()调用,或者通知 JVM 您将使用更多的本地引用。
清单 9 向 清单 8 中的示例添加了一个DeleteLocalRef()调用,用于通知 JVM 本地引用已不再需要,以及将可同时存在的本地引用的数量限制为一个合理的数值,而与数组的大小无关:
清单 9. 添加DeleteLocalRef()
void workOnArray(JNIEnv* env, jobject obj,jarray array){
jint i;
jint count =(*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobjectelement = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}
/* dosomething with array element */
(*env)->DeleteLocalRef(env, element);
}
}
您可以调用 JNI EnsureLocalCapacity()方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致FatalError。