现在你已经知道如何JNI是怎样让native代码访问基本类型和字符串、数组这种引用类型,下一步你将学习怎样与任意对象的成员(field)和方法(method)交互。除了访问成员域外,这一章包含了从native代码中调用Java实现的方法,一般被认为是是native代码执行回调。
我们将从介绍支持成员访问和方法回调的JNI函数开始。在本章的后续部分我们将讨论怎样使用简单但有效的缓存技术使得这种操作更高效。在最后一节中,我们将会讨论调用native方法以及从native代码访问成员和调用方法的性能特点(performance characteristic)。
4.1 访问成员
Java支持两种成员域。每一个类的实例拥有属于自己的类的实例成员(instance field)的copy,而一个类的所有实例共享累的静态成员(static field)。
JNI提供了相应的函数使得native代码可以用来获取和设置对象的实例成员和类的静态成员。让我们先来看一个例子,这个例子阐明了如何从一个native方法中访问实例成员。
classInstanceFieldAccess {
privateString s;
privatenative void accessField();
publicstatic void main(String args[]) {
InstanceFieldAccessc = new InstanceFieldAccess();
c.s= "abc";
c.accessField();
System.out.println("InJava:");
System.out.println("c.s = \"" + c.s + "\"");
}
static{
System.loadLibrary("InstanceFieldAccess");
}
}
InstanceFieldAccess类定义了一个实例成员s。main方法创建了一个对象,设置实例成员的值然后调用native方法InstanceFieldAccess.accessField。如我们稍后所见,native方法打印输出已存在的实例成员的值,然后为这个成员设置新的值。程序在native方法返回后再打印一次成员值,来证明成员值确实发生了改变。
这是InstanceFieldAccess.accessFieldnative方法的实现。
JNIEXPORTvoid JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv*env, jobject obj)
{
jfieldIDfid; //store the field ID
jstringjstr;
constchar *str;
//Geta reference to obj's class
jclasscls = (*env)->GetObjectClass(env,obj);
printf("InC:\n");
//Lookfor the instance field s in cls
fid= (*env)->GetFieldID(env, cls, "s","Ljava/lang/String;");
if(fid== NULL){
return;//failedto find the field
}
//Readthe instance field s
jstr= (*env)->GetObjectField(env, obj, fid);
str= (*env)->GetStringUTFChars(env, jstr, NULL);
if(str== NULL){
return;//outof memory
}
printf("c.s = \"%s\"\n",str);
(*env)->ReleaseStringUTFChars(env,jstr, str);
//Creata new string and overwrite the instance field
jstr= (*env)->NewStringUTF(env, "123");
if(jstr== NULL){
return;//outof memory
}
(*env)->SetObjectField(env,obj, fid, jstr);
}
与InstanceFieldAccessnative库一起运行InstanceFieldAccess类会产生如下输出:
InC:
c.s= "abc"
InJava:
c.s= "123"
4.1.1 访问实例成员的过程
为访问实例成员,native方法分两步处理(followa two-step process)。首先,调用GetFileID通过类引用、成员名、成员描述符(descriptor)获取成员ID(field ID):
fid= (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
示例代码通过对实例引用obj调用GetObjectClass(callGetObjectClass on instance reference obj)获取类引用cls,cls作为第二个参数传递给native方法。
一旦你获取了成员ID,你可以将对象引用和成员ID传给合适的实例访问函数:
jstr= (*env)->GetObjectField(env, obj, fid);
因为字符串和数组是特殊的对象类型,我们使用GetObjectField访问类型为字符串的实例成员。除Get/SetObjectField之外,JNI也支持其他函数比如GetIntField和SetFloatField访问类型为基本类型的实例成员。
4.1.2 成员描述符
你可能注意到在前面的章节中,我们使用一个特殊编码的C字符串"Ljava/lang/String;"来表示Java中的成员类型。这些C字符串叫做JNI成员描述符(JNIfield descriptors)。
成员声明的类型决定了字符串的内容。例如,你使用“I”代表int成员,“F”代表float成员,“D”代表double成员,“Z”代表boolean成员等等。
引用类型描述符,如java.lang.String,以字母L开头,后面跟着JNI类描述符(JNIclass descriptor),最后以分号结尾。在标准类名中的“.”分隔符加载JNI类描述符中换成了“/”。因此,你可以像下面这样为一个java.lang.String类型的成员构建成员描述符:
"Ljava/lang/String;"
数组类型的描述符包含“[”字符,后面跟着数组元素(component)类型的描述符。例如“[I”是int[]类型成员的描述符。12.3.3节中有详细的成员描述符信息和他们在java中对应的类型。
你可以使用javap工具(随JDK或java 2 SDK一同发布)来从类文件中生成成员描述符。正常情况下,javap在给定的类中输出方法和成员类型。如果你指明了“-s”选项(并且使用“-p”选项来导出私有成员),javap打印输出JNI描述符:
javap-s -p InstanceFieldAccess
反馈给你的输出包含成员s的JNI描述符:
...
sLjava/lang/String;
...
使用javap工具帮助消除手动完成JNI描述符可能发生的错误。
4.1.3 访问静态成员
访问静态成员与访问实例成员类似。让我们来看一个较之InstanceFiledAccess有轻微改变的例子:
classStaticFielcdAccess {
privatestatic int si;
privatenative void accessField();
publicstatic void main(String args[]) {
StaticFieldAccessc = new StaticFieldAccess();
StaticFieldAccess.si= 100;
c.accessField();
System.out.println("InJava:");
System.out.println("StaticFieldAccess.si = " + si);
}
static{
System.loadLibrary("StaticFieldAccess");
}
}
StaticFielcdAccess方法包含一个静态整型成员si。StaticFielcdAccess.main方法创建一个对象,初始化静态成员然后调用native方法StaticFieldAccess.accessField。我们稍后会看到,native方法会打印输出现有的静态成员的值,然后为之赋予新值。为了证明成员值确实发生了改变,native方法返回后,程序会再次打印静态成员的值。
这里有StaticFieldAccess.accessFieldnative方法的具体实现:
JNIEXPORTvoid JNICALL
Java_StaticFieldAccess_accessField(JNIEnv*env, jobject obj)
{
jfieldIDfid; /* store the field ID */
jintsi;
/*Get a reference to obj’s class */
jclasscls = (*env)->GetObjectClass(env, obj);
printf("InC:\n");
/*Look for the static field si in cls */
fid= (*env)->GetStaticFieldID(env, cls, "si", "I");
if(fid == NULL) {
return;/* field not found */
}
/*Access the static field si */
si= (*env)->GetStaticIntField(env, cls, fid);
printf("StaticFieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env,cls, fid, 200);
}
与本地库一起运行会产生如下输出:
InC:
StaticFieldAccess.si= 100
InJava:
StaticFieldAccess.si= 200
访问静态成员与访问实例成员之间有两点不同:
1、访问静态成员调用GetStaticFieldID方法,相反,访问实例成员调用GetFieldID方法。GetStaticFieldID和GetFieldID有相同的返回值jfieldID。
2、一旦你获得了静态成员的ID,你会传递类引用(classreference)而不是实例引用(Instance reference)给合适的静态成员访问函数。
4.2 调用方法(Calling Methods)
Java中的方法分为几类。实例方法(Instance Methods)必须在一个类的特定实例中被调用,然而,静态方法(static method)独立于任何实例被调用。我们将把关于构建(constructor)的讨论推迟到下一节。
JNI支持一整套函数,这些函数允许你从native方法中执行回调。下面的示例程序包含了一个native方法,这个方法可以调用Java语言编写的实例方法。
classInstanceMethodCall {
privatenative void nativeMethod();
privatevoid callback() {
System.out.println("InJava");
}
publicstatic void main(String args[]) {
InstanceMethodCallc = new InstanceMethodCall();
c.nativeMethod();
}
static{
System.loadLibrary("InstanceMethodCall");
}
}
native方法的实现如下:
JNIEXPORTvoid JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv*env, jobject obj)
{
jclasscls = (*env)->GetObjectClass(env, obj);
jmethodIDmid = (*env)->GetMethodID(env, cls, "callback", "()V");
if(mid== NULL){
return;//methodnot found
}
printf("InC\n");
(*env)->CallVoidMethod(env,obj, mid);
}
运行上面的程序产生下列输出:
InC
InJava
4.2.1 调用实例方法(Calling Instance Methods)
Java_InstanceMethodCall_nativeMethod的实现说明调用一个实例方法需要下面两步:
native方法首先调用JNI函数GetMethodID。GetMethodID在给定的类中查找该方法。这种查找基于方法的方法名以及类型描述符。如果方法不存在,那么GetMethodID返回NULL。在这一点(at this point),从native方法直接返回造成了NoSuchMethodError,这个异常会在调用InstanceMethodCall.nativeMethod的代码中抛出。
然后native方法调用CallVoidMethod。CallVoidMethod调用一个返回类型为void的实例方法(CallVoidMethodinvokes an instance method that has the return type void)。你需要传递对象、方法ID和实际参数(在上面的例子中不存在)给CallVoidMethod。
除了CallVoidMethod函数之外,JNI也支持其他返回类型的方法调用函数。例如,你进行回调的方法返回一个int类型的值,那么你的native方法将使用CallIntMethod。类似的,你也可以使用CallObjectMethod调用返回对象的函数,这些对象宝库奥java.lang.String实例和数组。
你可以使用Call<Type>Method系列的函数来调用接口方法。你必须从接口类型中得到方法的ID。例如,下面的代码段调用了java.lang.Tread实例的Runnable.run方法:
jobjectthd = ...; /* a java.lang.Thread instance */
jmethodIDmid;
jclassrunnableIntf =
(*env)->FindClass(env,"java/lang/Runnable");
if(runnableIntf == NULL) {
.../* error handling */
}
mid= (*env)->GetMethodID(env, runnableIntf, "run", "()V");
if(mid == NULL) {
.../* error handling */
}
(*env)->CallVoidMethod(env,thd, mid);
.../* check for possible exceptions */
我们在3.3.5小节中看到FindClass函数返回一个命名类的引用,这里我们使用它来获取一个命名接口的引用
4.2.2 构造方法描述符(Forming the Method Descriptor)
JNI使用描述符字符串表示方法类型,与如何表示成员类型的方式类似。一个方法描述符结合了参数类型和方法返回值类型。参数类型在前面并且在小括号里面。参数类型按照它们在方法声明中的顺序排列。多个参数类型中间没有分隔符。如果一个方法没有参数,表示为一对空的小括号。方法的返回值类型直接放在参数类型右闭括号后面。
例如,“(I)V”表示一个拥有一个int参数,返回值类型为void的方法。“()D”表示一个没有参数返回值为double的方法。不要让“intf(void)”这样的C语言函数原型误导你认为“(V)I”是一个有效的方法描述符。使用“()I”来替代。
方法描述符可能包含类描述符(12.3.2)。例如,方法:
nativeprivate String getLine(String);
描述符如下:
(Ljava/lang/String;)Ljava/lang/String;
数组类型的描述符以“[”字符开始,后面跟着数组元素的描述符。例如,有如下方法:
publicstatic void main(String[] args);
其描述符如下:
"([Ljava/lang/String;)V"
12.3.4小节中给出了怎样构建一个JNI方法描述符的完整描述。你可以使用javap工具打印输出JNI方法描述符。例如,运行:
javap-s -p InstanceMethodCall
你可以得到如下输出:
...
privatecallback ()V
publicstatic main ([Ljava/lang/String;)V
privatenative nativeMethod ()V
...
-s标志告诉javap输出JNI描述符字符串而不是它们在java中表现的类型。-p标志使javap在它的输出中包含关于类的私有成员的信息。
4.2.3 调用静态方法(Calling Static Methods)
前面的例子示范说明了native、代码如何调用实例方法。类似的,你可以通过下面几步从native代码中执行静态方法的回调:
1、使用GetStaticMethodID而不是GetMethodID获取方法ID。
2、传递类(thecalss)、方法ID和参数给一个静态方法调用函数,如:CallStaticVoidMethod, CallStaticBooleanMethod等等。
允许你访问静态方法的函数与允许你访问实例方法的函数有一个关键的不同。前者把一个类引用作为第二个参数,而后者把一个对象引用作为第二个参数。例如,你传递一个类引用给CallStaticVoidMethod,但是传递一个对象引用给CallVoidMethod。
在Java层,你可以使用两种可选择的语法:Cls.f或obj.f(obj是Cls的实例)来调用类Cls中的静态方法f。(后者是推荐使用的编程风格)在JNI中,当从native代码中调用静态方法时,你必须总是指明类引用。
让我们看一个从native进行静态方法回调的例子。较之之前的InstanceMethodCall有了微小的变化:
classStaticMethodCall {
privatenative void nativeMethod();
privatestatic void callback(){
System.out.println("InJava");
}
publicstatic void main(String args[]){
StaticMethodCallc = new StaticMethodCall();
c.nativeMethod();
}
static{
System.loadLibrary("StaticMethodCall");
}
}
下面是native方法的具体实现:
JNIEXPORTvoid
JNICALLJava_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclasscls = (*env)->GetObjectClass(env, obj);
jmethodIDmid = (*env)->GetStaticMethodID(env, cls, "callback","()V");
if(mid== NULL){
return;//methodnot found
}
printf("InC\n");
(*env)->CallStaticVoidMethod(env,cls, mid);
}
确认你传递了cls(以粗体突出显示)而不是obj给CallStaticVoidMethod。运行程序将产生下列预期输出:
InC
InJava
4.2.4调用超类的实例方法(Calling Instance Method of a Supercalss)
你可以调用超类中定义但是在对象所属的类中被重载(overridden)的实例方法。JNI提供了一套CallNonvirtual<Type>Method函数来实现这种功能(purpose)。为了调用超类中定义的实例方法,你可以像下面这样做:
1、使用GetMethodID而不是GetStaticMethodID从指向超类的引用获取方法ID。
2、传递对象、超类、方法ID和相关参数给非虚拟调用函数(nonvirtual invocation function)比如CallNonvirtualVoidMethod、CallNonvirtualBooleanMethod等等。
你需要调用超类实例方法的时候相对而言比较少见。这个方法(facility设备)与调用重载的超类方法类似,比如说f,在Java中使用如下结构(construct):
super.f();
CallNonvirtualVoidMethod也可以用来调用构造器(constructor),如下一节中会讲到的。
4.3 调用构造器(InvokingConstructor)
在JNI中,构造器(constructor)可以按照下面几步进行调用,与调用实例方法的步骤类似。为获取构造器(constructor)的方法ID,在方法描述符中传递“<init>”作为方法名,“V”作为返回值类型。然后你可以通过vhuandi方法ID给JNI函数如NewObject来调用构造器(constructor)。下面的代码实现了与JNI函数NewString具有相同功能的方法,可以通过C语言缓冲区存储的Unicode字符中构造一个java.lang.String对象:
jstring
MyNewString(JNIEnv*env, jchar *chars, jint len)
{
jclassstringClass;
jmethodIDcid;
jcharArrayelemArr;
jstringresult;
stringClass= (*env)->FindClass(env, "java/lang/String");
if(stringClass == NULL) {
returnNULL; /* exception thrown */
}
/*Get the method ID for the String(char[]) constructor */
cid= (*env)->GetMethodID(env, stringClass, "<init>","([C)V");
if(cid == NULL) {
returnNULL; /* exception thrown */
}
/*Create a char[] that holds the string characters */
elemArr= (*env)->NewCharArray(env, len);
if(elemArr == NULL) {
returnNULL; /* exception thrown */
}
(*env)->SetCharArrayRegion(env,elemArr, 0, len, chars);
/*Construct a java.lang.String object */
result= (*env)->NewObject(env, stringClass, cid, elemArr);
/*Free local references */
(*env)->DeleteLocalRef(env,elemArr);
(*env)->DeleteLocalRef(env,stringClass);
return result;
}
这个函数非常复杂,值得详细解释。首先FindClass返回一个指向java.lang.String的引用。然后GetMethodID返回字符串构造器,String(char[] chars),的方法ID。我们调用NewCharArray来为一个字符数组分配内存,用来保存字符串中字符。JNI函数NewObject调用方法ID指定的构造器。NewObject传入这么几个参数:要构造的类的引用,构造器方法ID和需要传递给构造器的参数。
DeleteLocalRef调用允许虚拟机释放局部引用(local reference)elemArr和stringClass适应的资源。5.2.1节中提供了什么时候以及为什么调用DeleteLocalRef的详细说明。
字符串是对象。这个例子更进一步突出说明了这个观点。然而这个例子也导致了一个问题。既然我们可以使用其他JNI函数实现相同的功能,为什么JNI提供了NewString这样的内置函数?原因就在于内置字符串函数远比从native中调用java.lang.String API高效。字符串是使用频率最高的对象类型,值得JNI提供特殊支持。
使用CallNonVirtualVoidMethod函数也可以调用构造器。在这种情况下,native代码必须首先通过调用AllocObject函数创建一个未初始化的对象。上面的NewObject调用:
result= (*env)->NewObject(env, stringClass, cid, elemArr);
可以用先调用AllocObject再调用CallNonvirtualVoidMethod来代替:
result= (*env)->AllocObject(env, stringClass);
if(result){
(*env)->CallNonvirtualVoidMethod(env,result, stringClass, cid, elemArr);
//weneed check for possible exceptions
if((*env)->ExceptionCheck(env)){
(*env)->DeleteLocalRef(env,result);
result= NULL;
}
}
AllocObject创建一个未初始化对象,使用的时候必须注意,一个构造器在每个对象中至多调用一次。native代码不应在一个对象中多次调用构造器。
有时候你会发现先创建(allocate为对象分配内存)未初始化对象然后随后调用构造器是很有用的。然而,在多数情况下,你应当使用NewObject,避免使用更易出错(error-prone)的AllocObject/CallNonvirtualVoidMethod函数对。
4.4 缓冲成员和方法ID(Caching Field and Method IDs)
获取成员和方法ID需要通过成员或方法的名字和描述符在符号表中查找。直接在符号表中查找代价高昂。在这一节中,我们介绍一种被用来减少开销的技术。
具体思想是计算成员和方法ID并存在缓存中方便后续重复的使用(The idea is to compute field and method IDs and cache them forrepeated uses later)。用缓存保存成员和方法ID有两种方法,取决于在使用成员和方法ID时进行缓存还是在定义成员或方法的类的静态初始化代码中进行缓存。
4.4.1 使用时进行缓存(Caching at the Point of Use)
成员和方法ID可以在native代码访问成员值或执行回调时进行缓存。下面Java_InstanceFieldAccess_accessField函数的实现中在静态变量中缓存成员ID,如此一来,在InstanceFieldAccess.accessField方法每次调用时不需要重复计算。
JNIEXPORTvoid JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv*env, jobject obj)
{
staticjfieldID fid_s = NULL; //cached field ID for s
jclasscls = (*env)->GetObjectClass(env, obj);
jstringjstr;
constchar *str;
if(fid_s== NULL){
fid_s= (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
if(fid_s== NULL){
return;//exceptionalready thrown
}
}
printf("InC");
jstr= (*env)->GetObjectField(env, obj, fid_s);
str= (*env)->GetStringUTFChars(env, jstr, NULL);
if(str== NULL){
return;//outof memory
}
(*env)->SetObjectField(env,obj, fid_s, jstr);
}
突出显示的静态变量fid_s用来存储预计算好的InstanceFieldAccess.s的成员ID。这个静态变量初始化为NULL。当InstanceFieldAccess.accessField方法第一次被调用时,它及孙成员ID并缓存在静态变量中以便稍后使用。
你可能注意到在上面的代码中存在明显的资源竞争(race condition)。多个线程可能在同一时间调用InstanceFieldAccess.accessField方法并计算相同的成员ID。
一个线程可能复写另一个线程计算的静态变量fid_s。幸运的是,虽然这种资源竞争导致多个线程的重复工作(duplicated work inmultiple thread),它却是无害的(it isotherwise harmless)。多个线程为同一个类的同一个成员变量计算的成员ID必须是相同的。
按照相同的思路,我们可以在前面的MyNewString实例中为java.lang.String构造器缓存方法ID:
jstringMyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclassstringClass;
jcharArrayelemArr;
staticjmethodID cid = NULL;
jstringresult;
stringClass= (*env)->FindClass(env, "java/lang/String");
if(stringClass == NULL){
return NULL; //exceptionthrown
}
//notethat cid is a static variable
if(cid== NULL) {
//Getthe method ID for the String constructor
cid== (*env)->GetMethodID(env, stringClass, "<init>","([C)V");
if(cid== NULL){
returnNULL;//exception thrown
}
}
//createa char[] that holds the string characters
elemArr= (*env)->NewCharArray(env, len);
if(elemArr== NULL){
return;//exceptionthrown
}
(*env)->SetCharArrayRegion(*env,elemArr, 0, len, chars);
//constructa java.lang.String object
result= (*env)->NewObject(env, stringClass, cid, elemArr);
//freelocal reference
(*env)->DeleteLocalRef(env,elemArr);
((env)->DeleteLocalRef(env,stringClass);
returnresult;
}
当MyNewString第一次被调用时,计算java.lang.String构造器的方法ID。突出此案时的静态变量cid缓存计算结果。
4.2.2 在定义类的初始化程序中缓存(Caching in the Defining Class's Initializer)
当我们在使用时缓存成员或方法ID的时候,我们必须检查ID是否已经被缓存。如果ID早已存在的话,这种方式不仅会在快速路径(fast psth)上导致小的性能损失,而且会导致多次重复检查与缓存。例如,如果多个native方法都需要访问同一个成员,那么他们都需要检查和计算相应的成员ID。
在许多情况下,在应用能调用native方法前初始化native方法需要的成员和方法ID是很方便的。虚拟机总是在调用类的任何方法之前执行类的静态初始化程序。因此,在定义成员或方法的类的静态初始化程序是一个适合放置计算和缓存成员或方法ID的地方。
例如,为缓存InstanceMethodCall.callback的方法ID,我们引入一个新的native方法initIDs,从InstanceMethodCall类的静态初始化程序中调用:
classInstanceMethodCall{
privatestatic native void initIDs();
privatenative void nativeMethod();
privatevoid callback(){
System.out.println("Injava");
}
publicstatic void main(String args[]){
InstanceMethodCallc = new InstanceMethodCall();
c.nativeMethod();
}
static{
System.loadLibrary("InstanceMethodCall");
initIDs();
}
}
与4.2节中的初始代码相比,上面的程序多了两行。initIDs的实现只是为InstanceMethodCall.callback简单的计算并缓存方法ID:
jmethodIDMID_InstaceMethodCall_callback;
JNIEXPORTvoid JNICALL
Java_InstanceMethodCall_initIds(JNIEnv*env, jclass cls)
{
MID_InstaceMethodCall_callback= (*env)->GetMethodID(env, cls, "callback", "()V");
}
虚拟机运行静态初始化程序,接着在执行InstanceMethodCall类的任何方法之前调用initIDs方法。因为方法ID早已缓存在全局变量中,InstanceMethodCall.nativeMethodd的native实现不需要再去查找符号表:
JNIEXPORTvoid JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv*env, jobject obj)
{
printf("InC\n");
(*env)->CallVoidMethod(env,obj, MID_InstaceMethodCall_callback);
}
4.4.3 两种缓存ID方法的比较(Comparison between the Two Approaches to Caching IDs)
如果JNI程序员不能控制定义成员或方法的类的源码,那么在使用是缓存ID是比较合适的。例如,在MyNewString实例中,我们不能在java.lang.String类中插入initIDs native方法来预计算和存储java.lang.String构造器的方法ID。突出此案时的静态变量cid缓存计算结果。
当与在定义类的初始化程序中进行缓存比较时,使用时进行缓存有着诸多劣势。
1、正如之前解释的那样,使用时进行缓存需要在执行快速路径(in the execution fast path)进行检查,并且可能需要对同一个成员或方法ID进行多次检查与初始化。
2、直到类被卸载,方法或成员ID都是有效的。如果你在使用时缓存类和方法ID,那么你必须确定当native代码仍然使用缓存的ID的值时,定义类不能被卸载和重载(下一章将会告诉你怎样通过使用JNI创建指向一个类的引用以保证其不被卸载)。另一方面,如果缓存在定义类的静态初始化程序中完成,那么当类被卸载和重载时,缓存ID自动重新计算。
因此,可行方案是(wherefeasible),优先选择在定义类的静态初始化代码中缓存成员和方法ID。
4.5 JNI成员和方法操作的性能(Performance of JNI Field and Method Operations)
当学习了怎样通过缓存成员和方法ID提高性能(enhance performance)后,你可能会奇怪:使用JNI访问成员和调用方法的工作特性(performance characteristics)是什么?从native代码执行回调(native/java回调)的开销与调用native方法(java/native调用)的开销相比怎么样?与常规方法调用(java/java调用)相比呢?
这个问题的答案毫无疑问依赖于底层虚拟机实现JNI有多么高效。因此不可能给出一个适用于各种虚拟机实现的明确的工作特性。相反,我们会分析调用native方法固有的开销以及JNI成员和方法的开销,然后为JNI程序员和实现者(implementor)给出一个通用的性能指标(performance)。
让我们从比较Java/native调用和Java/Java调用的开销开始。由于以下原因,Java/native调用可能比Java/Java调用慢:
1、与使用Java虚拟机内置的Java/java调用遵循的调用约定相比,native方法更有可能遵循不同的调用约定。这样做的结果是,虚拟机必须在跳转至native方法入口点之前执行额外的操作来建立参数和构建栈。
2、虚拟机一般会内联方法调用。内联Java/native调用比内联Java/Java调用麻烦许多。
我们估计一个典型的Java虚拟机执行java/native调用可能比执行Java/Java调用慢两到三倍。因为Java/Java调用只需要几个周期,而除非native代码执行简单操作,额外的开销才可以忽略不计。构建一个虚拟机使得Java/native调用的性能与Java/Java调用的性能接近或相同是可能的。(例如,这样一个虚拟机实现可能会采用与内部的Java/Java调用约定相同的JNI调用约定)。
一个native/Java回调的工作特性(performancecharacteristic是)技术上类似于一个Java/native调用。理论上来说,一个native/Java回调的开销也是Java/Java调用的两到三倍。然而,实际上,native/Java回调相当少见。虚拟机一般不会优化(optimize)回调性能。在这种情况下(at the time of),写的一个虚拟机的实现产品中,native/java回调与Java/Java调用相比可能会花费10倍乃至更高代价。
使用JNI访问成员的开销在于通过JNIEnv进行调用的开销。native代码执行一个C函数调用来取得对象的值而不是直接获取对象的值。函数调用是必须的,因为它使native代码与虚拟机维护的内部对象表示(internal object representation)相互分离。JNI访问成员的开销通常可以忽略,因为一个函数调用的花费只需要几个周期。